fix(security): scope introspection cache keys to the middleware instance

The introspection cache used the raw token as key in a shared cache.
Two instances with different providers or clients shared results. Keys
now use the same instance prefix as the verified-token cache.
This commit is contained in:
2026-11-03 01:18:12 +00:00
parent 6e522fd212
commit 2f0ee00e51
4 changed files with 34 additions and 3 deletions
+9
View File
@@ -309,6 +309,15 @@ func (tc *TokenCache) SetNamespace(ns string) {
tc.ns.Store(ns) tc.ns.Store(ns)
} }
// Namespace returns the scope set by SetNamespace, or "" when none is set.
func (tc *TokenCache) Namespace() string {
if tc == nil {
return ""
}
ns, _ := tc.ns.Load().(string)
return ns
}
func (tc *TokenCache) key(token string) string { func (tc *TokenCache) key(token string) string {
if ns, _ := tc.ns.Load().(string); ns != "" { if ns, _ := tc.ns.Load().(string); ns != "" {
return "t-" + ns + "|" + token return "t-" + ns + "|" + token
+13
View File
@@ -54,3 +54,16 @@ func TestTokenCacheEmptyNamespaceKeepsLegacyKey(t *testing.T) {
t.Fatal("namespace-less caches must keep sharing the legacy key") t.Fatal("namespace-less caches must keep sharing the legacy key")
} }
} }
func TestIntrospectionKeyScopedPerInstance(t *testing.T) {
a := &TraefikOidc{tokenCache: &TokenCache{}}
b := &TraefikOidc{tokenCache: &TokenCache{}}
a.tokenCache.SetNamespace("p|app-a|app-a")
b.tokenCache.SetNamespace("p|app-b|app-b")
if a.introspectionKey("tok") == b.introspectionKey("tok") {
t.Fatal("different instances share an introspection cache key")
}
if got := (&TraefikOidc{}).introspectionKey("tok"); got != "tok" {
t.Fatalf("namespace-less key = %q, want raw token", got)
}
}
+11 -2
View File
@@ -53,7 +53,7 @@ type IntrospectionResponse struct {
func (t *TraefikOidc) introspectToken(token string) (*IntrospectionResponse, error) { func (t *TraefikOidc) introspectToken(token string) (*IntrospectionResponse, error) {
// Check cache first // Check cache first
if t.introspectionCache != nil { if t.introspectionCache != nil {
if cached, found := t.introspectionCache.Get(token); found { if cached, found := t.introspectionCache.Get(t.introspectionKey(token)); found {
if response, ok := cached.(*IntrospectionResponse); ok { if response, ok := cached.(*IntrospectionResponse); ok {
t.logger.Debugf("Using cached introspection result for token") t.logger.Debugf("Using cached introspection result for token")
return response, nil return response, nil
@@ -173,7 +173,7 @@ func (t *TraefikOidc) introspectToken(token string) (*IntrospectionResponse, err
cacheDuration = untilExp cacheDuration = untilExp
} }
} }
t.introspectionCache.Set(token, &introspectionResp, cacheDuration) t.introspectionCache.Set(t.introspectionKey(token), &introspectionResp, cacheDuration)
t.logger.Debugf("Cached introspection result for %v", cacheDuration) t.logger.Debugf("Cached introspection result for %v", cacheDuration)
} }
@@ -280,3 +280,12 @@ func (t *TraefikOidc) validateOpaqueToken(token string) error {
t.logger.Debugf("Opaque token validation successful via introspection") t.logger.Debugf("Opaque token validation successful via introspection")
return nil return nil
} }
// introspectionKey scopes an introspection verdict to this instance's
// provider, client and audience, like the verified-token cache.
func (t *TraefikOidc) introspectionKey(token string) string {
if ns := t.tokenCache.Namespace(); ns != "" {
return ns + "|" + token
}
return token
}
+1 -1
View File
@@ -812,7 +812,7 @@ func (t *TraefikOidc) RevokeToken(token string) {
// from the introspection cache after revocation. Next request // from the introspection cache after revocation. Next request
// re-introspects and sees active=false. // re-introspects and sees active=false.
if t.introspectionCache != nil { if t.introspectionCache != nil {
t.introspectionCache.Delete(token) t.introspectionCache.Delete(t.introspectionKey(token))
} }
d := blacklistDuration(token) d := blacklistDuration(token)