Support additional verification of the token to ensure OIDC compliance

This commit is contained in:
2024-11-06 11:32:35 +00:00
parent 6196a72a8e
commit d6fd3467c3
4 changed files with 92 additions and 19 deletions
+29
View File
@@ -94,6 +94,8 @@ func (t *TraefikOidc) getNewTokenWithRefreshToken(refreshToken string) (*TokenRe
response.RefreshToken = refreshToken response.RefreshToken = refreshToken
} }
t.logger.Debug("Token response: %+v", response)
return response, nil return response, nil
} }
@@ -181,12 +183,39 @@ func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request)
return false, "" return false, ""
} }
// Verify the nonce
nonceFromToken, ok := claims["nonce"].(string)
if !ok || nonceFromToken == "" {
t.logger.Error("Nonce claim is missing in ID Token")
handleError(rw, "Nonce verification failed", http.StatusUnauthorized, t.logger)
return false, ""
}
sessionNonce, ok := session.Values["nonce"].(string)
if !ok || sessionNonce == "" {
t.logger.Error("Nonce is missing in session")
handleError(rw, "Nonce verification failed", http.StatusUnauthorized, t.logger)
return false, ""
}
if nonceFromToken != sessionNonce {
t.logger.Error("Nonce mismatch")
handleError(rw, "Nonce verification failed", http.StatusUnauthorized, t.logger)
return false, ""
}
// Remove 'nonce' and 'csrf' from session after successful verification
delete(session.Values, "nonce")
delete(session.Values, "csrf")
email, _ := claims["email"].(string) email, _ := claims["email"].(string)
session.Values["authenticated"] = true session.Values["authenticated"] = true
session.Values["id_token"] = rawIDToken session.Values["id_token"] = rawIDToken
session.Values["refresh_token"] = oauth2Token["refresh_token"] session.Values["refresh_token"] = oauth2Token["refresh_token"]
session.Values["email"] = email session.Values["email"] = email
session.Values["access_token"] = oauth2Token["access_token"]
if err := session.Save(req, rw); err != nil { if err := session.Save(req, rw); err != nil {
handleError(rw, "Failed to save session", http.StatusInternalServerError, t.logger) handleError(rw, "Failed to save session", http.StatusInternalServerError, t.logger)
return false, "" return false, ""
+19 -3
View File
@@ -82,9 +82,25 @@ func fetchJWKS(jwksURL string, httpClient *http.Client) (*JWKSet, error) {
return &jwks, nil return &jwks, nil
} }
func verifyAudience(tokenAudience, expectedAudience string) error { func verifyAudience(tokenAudience interface{}, expectedAudience string) error {
if tokenAudience != expectedAudience { switch aud := tokenAudience.(type) {
return fmt.Errorf("invalid audience") case string:
if aud != expectedAudience {
return fmt.Errorf("invalid audience")
}
case []interface{}:
found := false
for _, v := range aud {
if str, ok := v.(string); ok && str == expectedAudience {
found = true
break
}
}
if !found {
return fmt.Errorf("invalid audience")
}
default:
return fmt.Errorf("invalid 'aud' claim type")
} }
return nil return nil
} }
+25 -4
View File
@@ -46,22 +46,43 @@ func parseJWT(token string) (*JWT, error) {
func (j *JWT) Verify(issuerURL, clientID string) error { func (j *JWT) Verify(issuerURL, clientID string) error {
claims := j.Claims claims := j.Claims
if err := verifyIssuer(claims["iss"].(string), issuerURL); err != nil { iss, ok := claims["iss"].(string)
if !ok {
return fmt.Errorf("missing 'iss' claim")
}
if err := verifyIssuer(iss, issuerURL); err != nil {
return err return err
} }
if err := verifyAudience(claims["aud"].(string), clientID); err != nil { aud, ok := claims["aud"]
if !ok {
return fmt.Errorf("missing 'aud' claim")
}
if err := verifyAudience(aud, clientID); err != nil {
return err return err
} }
if err := verifyExpiration(claims["exp"].(float64)); err != nil { exp, ok := claims["exp"].(float64)
if !ok {
return fmt.Errorf("missing or invalid 'exp' claim")
}
if err := verifyExpiration(exp); err != nil {
return err return err
} }
if err := verifyIssuedAt(claims["iat"].(float64)); err != nil { iat, ok := claims["iat"].(float64)
if !ok {
return fmt.Errorf("missing or invalid 'iat' claim")
}
if err := verifyIssuedAt(iat); err != nil {
return err return err
} }
sub, ok := claims["sub"].(string)
if !ok || sub == "" {
return fmt.Errorf("missing or empty 'sub' claim")
}
return nil return nil
} }
+19 -12
View File
@@ -69,7 +69,7 @@ var defaultExcludedURLs = map[string]struct{}{
} }
func (t *TraefikOidc) VerifyToken(token string) error { func (t *TraefikOidc) VerifyToken(token string) error {
t.logger.Debugf("Verifying token") t.logger.Debugf("Verifying token: %s", token)
if !t.limiter.Allow() { if !t.limiter.Allow() {
return fmt.Errorf("rate limit exceeded") return fmt.Errorf("rate limit exceeded")
} }
@@ -325,7 +325,12 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
authenticated, needsRefresh, expired := t.isUserAuthenticated(session) authenticated, needsRefresh, expired := t.isUserAuthenticated(session)
if expired || !authenticated { if expired {
t.handleExpiredToken(rw, req, session)
return
}
if !authenticated {
t.initiateAuthentication(rw, req, session, t.redirectURL) t.initiateAuthentication(rw, req, session, t.redirectURL)
return return
} }
@@ -333,7 +338,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if needsRefresh { if needsRefresh {
refreshed := t.refreshToken(rw, req, session) refreshed := t.refreshToken(rw, req, session)
if !refreshed { if !refreshed {
t.initiateAuthentication(rw, req, session, t.redirectURL) t.handleExpiredToken(rw, req, session)
return return
} }
} }
@@ -430,21 +435,21 @@ func (t *TraefikOidc) isUserAuthenticated(session *sessions.Session) (bool, bool
return false, false, true // Can't read claims, consider it expired return false, false, true // Can't read claims, consider it expired
} }
exp, ok := claims["exp"].(float64) expClaim, ok := claims["exp"].(float64)
if !ok { if !ok {
t.logger.Errorf("Failed to get expiration time from claims") t.logger.Errorf("Failed to get expiration time from claims")
return false, false, true // No expiration, consider it expired return false, false, true // No expiration, consider it expired
} }
now := time.Now().Unix() now := time.Now().Unix()
expTime := int64(exp) expTime := int64(expClaim)
if now > expTime { if now > expTime {
return false, false, true // Token has expired return false, false, true // Token has expired
} }
gracePeriod := time.Minute * 5 gracePeriod := time.Minute * 5
if time.Now().Add(gracePeriod).Unix() > expTime { if now+int64(gracePeriod.Seconds()) > expTime {
return true, true, false // Token will expire soon, needs refresh return true, true, false // Token will expire soon, needs refresh
} }
@@ -457,17 +462,19 @@ func (t *TraefikOidc) initiateAuthentication(rw http.ResponseWriter, req *http.R
session.Values["incoming_path"] = req.URL.Path session.Values["incoming_path"] = req.URL.Path
t.logger.Debugf("Setting CSRF token: %s", csrfToken) t.logger.Debugf("Setting CSRF token: %s", csrfToken)
if err := session.Save(req, rw); err != nil {
t.logger.Errorf("Failed to save session: %v", err)
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
return
}
nonce, err := generateNonce() nonce, err := generateNonce()
if err != nil { if err != nil {
http.Error(rw, "Failed to generate nonce", http.StatusInternalServerError) http.Error(rw, "Failed to generate nonce", http.StatusInternalServerError)
return return
} }
session.Values["nonce"] = nonce
t.logger.Debugf("Setting nonce: %s", nonce)
if err := session.Save(req, rw); err != nil {
t.logger.Errorf("Failed to save session: %v", err)
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
return
}
authURL := t.buildAuthURL(redirectURL, csrfToken, nonce) authURL := t.buildAuthURL(redirectURL, csrfToken, nonce)
http.Redirect(rw, req, authURL, http.StatusFound) http.Redirect(rw, req, authURL, http.StatusFound)