From 2cb42aa4859e2b1c37159b91accfefe8644e4d4b Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Tue, 3 Nov 2026 01:01:52 +0000 Subject: [PATCH] perf(logout): parse the ID token once for the invalidation check With backchannel or frontchannel logout on, every authenticated request parsed the ID token twice to read sid, sub and iat. One shared parse now returns the same values. A test pins equality with the two separate calls for valid, partial and malformed tokens. The gain is one parseJWT call fewer per request. I did not measure it under yaegi. --- logout.go | 16 ++++++++++++++ logout_invalidation_inputs_test.go | 34 ++++++++++++++++++++++++++++++ middleware.go | 6 ++---- 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 logout_invalidation_inputs_test.go 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