mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-12 04:04:26 +00:00
Support additional verification of the token to ensure OIDC compliance
This commit is contained in:
+29
@@ -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, ""
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user