diff --git a/helpers.go b/helpers.go index fb7580f..204529b 100644 --- a/helpers.go +++ b/helpers.go @@ -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, "" diff --git a/jwk.go b/jwk.go index a5e07df..e79c657 100644 --- a/jwk.go +++ b/jwk.go @@ -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 } diff --git a/jwt.go b/jwt.go index c97d71d..dc2fa61 100644 --- a/jwt.go +++ b/jwt.go @@ -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 } diff --git a/main.go b/main.go index b68da30..2e2c91d 100644 --- a/main.go +++ b/main.go @@ -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)