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.
This commit is contained in:
2026-11-03 01:01:52 +00:00
parent 9ecd987416
commit 2cb42aa485
3 changed files with 52 additions and 4 deletions
+16
View File
@@ -639,6 +639,22 @@ func (t *TraefikOidc) sessionCreatedAtForInvalidation(idToken string, session *S
return time.Time{} // zero: treated as invalidated -> safe re-auth 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 // extractSessionInfo extracts sid and sub from an ID token for session tracking
func (t *TraefikOidc) extractSessionInfo(idToken string) (sid, sub string, createdAt time.Time) { func (t *TraefikOidc) extractSessionInfo(idToken string) (sid, sub string, createdAt time.Time) {
if idToken == "" { if idToken == "" {
+34
View File
@@ -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)
}
})
}
}
+2 -4
View File
@@ -1054,8 +1054,7 @@ func (t *TraefikOidc) processAuthorizedRequestRS(rw http.ResponseWriter, req *ht
idToken := rs.idToken idToken := rs.idToken
if t.enableBackchannelLogout || t.enableFrontchannelLogout { if t.enableBackchannelLogout || t.enableFrontchannelLogout {
if idToken != "" { if idToken != "" {
sid, sub, _ := t.extractSessionInfo(idToken) sid, sub, createdAt := t.invalidationInputs(idToken, session)
createdAt := t.sessionCreatedAtForInvalidation(idToken, session)
if t.isSessionInvalidated(sid, sub, createdAt) { if t.isSessionInvalidated(sid, sub, createdAt) {
t.logger.Infof("Session for user %s has been invalidated via IdP-initiated logout", userIdentifier) t.logger.Infof("Session for user %s has been invalidated via IdP-initiated logout", userIdentifier)
if err := session.Clear(req, rw); err != nil { 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 { if t.enableBackchannelLogout || t.enableFrontchannelLogout {
idToken := session.GetIDToken() idToken := session.GetIDToken()
if idToken != "" { if idToken != "" {
sid, sub, _ := t.extractSessionInfo(idToken) sid, sub, createdAt := t.invalidationInputs(idToken, session)
createdAt := t.sessionCreatedAtForInvalidation(idToken, session)
if t.isSessionInvalidated(sid, sub, createdAt) { if t.isSessionInvalidated(sid, sub, createdAt) {
t.logger.Infof("Session for user %s has been invalidated via IdP-initiated logout", userIdentifier) t.logger.Infof("Session for user %s has been invalidated via IdP-initiated logout", userIdentifier)
// Clear the session and redirect to login // Clear the session and redirect to login