diff --git a/logout.go b/logout.go index fe94d17..3cceccb 100644 --- a/logout.go +++ b/logout.go @@ -639,6 +639,22 @@ func (t *TraefikOidc) sessionCreatedAtForInvalidation(idToken string, session *S return time.Time{} // zero: treated as invalidated -> safe re-auth } +// invalidationInputs returns sid, sub and the creation time for the IdP-logout +// check from one parseJWT call, with the same results as extractSessionInfo +// and sessionCreatedAtForInvalidation called separately. +func (t *TraefikOidc) invalidationInputs(idToken string, session *SessionData) (sid, sub string, createdAt time.Time) { + jwt, err := parseJWT(idToken) + if err != nil { + return "", "", t.sessionCreatedAtForInvalidation("", session) + } + sid, _ = jwt.Claims["sid"].(string) + sub, _ = jwt.Claims["sub"].(string) + if iat, ok := jwt.Claims["iat"].(float64); ok { + return sid, sub, time.Unix(int64(iat), 0) + } + return sid, sub, t.sessionCreatedAtForInvalidation("", session) +} + // extractSessionInfo extracts sid and sub from an ID token for session tracking func (t *TraefikOidc) extractSessionInfo(idToken string) (sid, sub string, createdAt time.Time) { if idToken == "" { diff --git a/logout_invalidation_inputs_test.go b/logout_invalidation_inputs_test.go new file mode 100644 index 0000000..256d29c --- /dev/null +++ b/logout_invalidation_inputs_test.go @@ -0,0 +1,34 @@ +package traefikoidc + +import ( + "testing" + + "github.com/gorilla/sessions" +) + +// invalidationInputs must return exactly what extractSessionInfo and +// sessionCreatedAtForInvalidation return when called separately. +func TestInvalidationInputsMatchSeparateCalls(t *testing.T) { + oidc := &TraefikOidc{logger: newNoOpLogger()} + sess := &SessionData{mainSession: &sessions.Session{Values: map[interface{}]interface{}{"created_at": int64(1700000000)}}} + + tokens := map[string]string{ + "all claims": makeTestJWT(t, map[string]interface{}{"sid": "s1", "sub": "u1", "iat": float64(1700000100)}), + "no iat": makeTestJWT(t, map[string]interface{}{"sid": "s1", "sub": "u1"}), + "sid only": makeTestJWT(t, map[string]interface{}{"sid": "s1", "iat": float64(1700000200)}), + "sub only": makeTestJWT(t, map[string]interface{}{"sub": "u1"}), + "wrong types": makeTestJWT(t, map[string]interface{}{"sid": 5, "sub": true, "iat": "x"}), + "malformed": "not-a-jwt", + "empty": "", + } + for name, tok := range tokens { + t.Run(name, func(t *testing.T) { + wantSID, wantSub, _ := oidc.extractSessionInfo(tok) + wantAt := oidc.sessionCreatedAtForInvalidation(tok, sess) + gotSID, gotSub, gotAt := oidc.invalidationInputs(tok, sess) + if gotSID != wantSID || gotSub != wantSub || !gotAt.Equal(wantAt) { + t.Fatalf("got (%q,%q,%v) want (%q,%q,%v)", gotSID, gotSub, gotAt, wantSID, wantSub, wantAt) + } + }) + } +} diff --git a/middleware.go b/middleware.go index 8fa931d..b118056 100644 --- a/middleware.go +++ b/middleware.go @@ -1054,8 +1054,7 @@ func (t *TraefikOidc) processAuthorizedRequestRS(rw http.ResponseWriter, req *ht idToken := rs.idToken if t.enableBackchannelLogout || t.enableFrontchannelLogout { if idToken != "" { - sid, sub, _ := t.extractSessionInfo(idToken) - createdAt := t.sessionCreatedAtForInvalidation(idToken, session) + sid, sub, createdAt := t.invalidationInputs(idToken, session) if t.isSessionInvalidated(sid, sub, createdAt) { t.logger.Infof("Session for user %s has been invalidated via IdP-initiated logout", userIdentifier) if err := session.Clear(req, rw); err != nil { @@ -1147,8 +1146,7 @@ func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http if t.enableBackchannelLogout || t.enableFrontchannelLogout { idToken := session.GetIDToken() if idToken != "" { - sid, sub, _ := t.extractSessionInfo(idToken) - createdAt := t.sessionCreatedAtForInvalidation(idToken, session) + sid, sub, createdAt := t.invalidationInputs(idToken, session) if t.isSessionInvalidated(sid, sub, createdAt) { t.logger.Infof("Session for user %s has been invalidated via IdP-initiated logout", userIdentifier) // Clear the session and redirect to login