From 83da8e0bc2b6932990c336404adc86ae61a103c0 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Tue, 3 Nov 2026 01:31:35 +0000 Subject: [PATCH] fix(security): scope logout invalidation keys to the middleware instance Back-channel logout keys for sub, sid and jti had no issuer or client in them, in a cache shared by every instance. A logout for user u1 in one realm logged out u1 in another, and an equal jti in two realms looked like a replay. Keys now carry the instance prefix. An instance without a prefix keeps the old key. --- logout.go | 5 +++++ logout_key_scope_test.go | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 logout_key_scope_test.go diff --git a/logout.go b/logout.go index 3cceccb..a66acbc 100644 --- a/logout.go +++ b/logout.go @@ -617,6 +617,11 @@ func (t *TraefikOidc) isSessionInvalidated(sid, sub string, sessionCreatedAt tim // buildSessionInvalidationKey creates a cache key for session invalidation func (t *TraefikOidc) buildSessionInvalidationKey(keyType, value string) string { + // sub, sid and jti are unique only within an issuer; the cache is shared by + // every instance, so scope the key to this instance's provider and client. + if ns := t.tokenCache.Namespace(); ns != "" { + return fmt.Sprintf("session_invalidation:%s|%s:%s", ns, keyType, value) + } return fmt.Sprintf("session_invalidation:%s:%s", keyType, value) } diff --git a/logout_key_scope_test.go b/logout_key_scope_test.go new file mode 100644 index 0000000..b9e72aa --- /dev/null +++ b/logout_key_scope_test.go @@ -0,0 +1,18 @@ +package traefikoidc + +import "testing" + +func TestSessionInvalidationKeyScopedPerInstance(t *testing.T) { + a := &TraefikOidc{tokenCache: &TokenCache{}} + b := &TraefikOidc{tokenCache: &TokenCache{}} + a.tokenCache.SetNamespace("https://kc/realms/a|c|c") + b.tokenCache.SetNamespace("https://kc/realms/b|c|c") + for _, kind := range []string{"sub", "sid", "jti"} { + if a.buildSessionInvalidationKey(kind, "u1") == b.buildSessionInvalidationKey(kind, "u1") { + t.Errorf("%s key shared across issuers", kind) + } + } + if got := (&TraefikOidc{}).buildSessionInvalidationKey("sub", "u1"); got != "session_invalidation:sub:u1" { + t.Errorf("namespace-less key changed: %q", got) + } +}