Support additional verification of the token to ensure OIDC compliance

This commit is contained in:
2024-10-03 14:00:44 +01:00
parent d06f9fcf90
commit 23afcad2ba
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
}
t.logger.Debug("Token response: %+v", response)
return response, nil
}
@@ -181,12 +183,39 @@ func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request)
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)
session.Values["authenticated"] = true
session.Values["id_token"] = rawIDToken
session.Values["refresh_token"] = oauth2Token["refresh_token"]
session.Values["email"] = email
session.Values["access_token"] = oauth2Token["access_token"]
if err := session.Save(req, rw); err != nil {
handleError(rw, "Failed to save session", http.StatusInternalServerError, t.logger)
return false, ""
+19 -3
View File
@@ -82,9 +82,25 @@ func fetchJWKS(jwksURL string, httpClient *http.Client) (*JWKSet, error) {
return &jwks, nil
}
func verifyAudience(tokenAudience, expectedAudience string) error {
if tokenAudience != expectedAudience {
return fmt.Errorf("invalid audience")
func verifyAudience(tokenAudience interface{}, expectedAudience string) error {
switch aud := tokenAudience.(type) {
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
}
+25 -4
View File
@@ -46,22 +46,43 @@ func parseJWT(token string) (*JWT, error) {
func (j *JWT) Verify(issuerURL, clientID string) error {
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
}
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
}
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
}
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
}
sub, ok := claims["sub"].(string)
if !ok || sub == "" {
return fmt.Errorf("missing or empty 'sub' claim")
}
return nil
}
+19 -12
View File
@@ -69,7 +69,7 @@ var defaultExcludedURLs = map[string]struct{}{
}
func (t *TraefikOidc) VerifyToken(token string) error {
t.logger.Debugf("Verifying token")
t.logger.Debugf("Verifying token: %s", token)
if !t.limiter.Allow() {
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)
if expired || !authenticated {
if expired {
t.handleExpiredToken(rw, req, session)
return
}
if !authenticated {
t.initiateAuthentication(rw, req, session, t.redirectURL)
return
}
@@ -333,7 +338,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if needsRefresh {
refreshed := t.refreshToken(rw, req, session)
if !refreshed {
t.initiateAuthentication(rw, req, session, t.redirectURL)
t.handleExpiredToken(rw, req, session)
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
}
exp, ok := claims["exp"].(float64)
expClaim, ok := claims["exp"].(float64)
if !ok {
t.logger.Errorf("Failed to get expiration time from claims")
return false, false, true // No expiration, consider it expired
}
now := time.Now().Unix()
expTime := int64(exp)
expTime := int64(expClaim)
if now > expTime {
return false, false, true // Token has expired
}
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
}
@@ -457,17 +462,19 @@ func (t *TraefikOidc) initiateAuthentication(rw http.ResponseWriter, req *http.R
session.Values["incoming_path"] = req.URL.Path
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()
if err != nil {
http.Error(rw, "Failed to generate nonce", http.StatusInternalServerError)
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)
http.Redirect(rw, req, authURL, http.StatusFound)