Summary
The token exchange cache (authbridge/authlib/plugins/tokenexchange/cache/cache.go) keys on SHA-256(subjectToken || \0 || audience) and has no awareness of the JWT jti claim. When a user token is revoked at Keycloak, cached exchanged tokens remain valid until their TTL expires (expires_in - 30s). This creates a revocation-blind window.
This was discovered during a security review where a leaked user token continued to be honored by AuthBridge because the exchanged token was served from cache, bypassing any upstream revocation check.
Proposed Changes
1. Store jti in cache entries
Add the subject token's jti claim to the cache entry so it can be checked against a revocation set on lookup:
type entry struct {
token string
jti string // from subject token's jti claim
expiresAt time.Time
}
Set() gains a jti parameter. The caller extracts the jti from the inbound subject token (standard JWT claim, included by Keycloak by default) before calling Set().
2. Add a revocation set to the cache
type Cache struct {
mu sync.RWMutex
entries map[string]entry
revoked map[string]time.Time // jti -> expiry (auto-cleanup)
maxSize int
}
New behavior:
Revoke(jti string, expiresAt time.Time) — adds the jti to the revoked set. expiresAt matches the original token's exp so entries self-clean (no point tracking revocation past token expiry).
Get() — after finding a cache hit, checks revoked[entry.jti]. If present and not yet expired, returns a miss (forces a fresh token exchange).
evictExpired() — also sweeps expired revocation entries.
3. Admin endpoint to trigger revocation
Add an HTTP endpoint on the ext-proc process (e.g., POST /admin/revoke accepting {"jti": "...", "exp": ...}) so that an external actor — operator, incident response tooling, or a Keycloak event listener — can push revocations into the cache.
Out of scope (follow-ups)
- Keycloak backchannel logout / event listener integration
- Redis-backed revocation set (in-memory is fine — revocation entries are small and short-lived, bounded by token lifetime)
- Revocation propagation across replicas (single-pod AuthBridge sidecars make this moot for now)
Motivation
Identified during a security review of an agentic AI deployment using AuthBridge for token exchange. A leaked user token was used by an AI agent, and even after the token was revoked in Keycloak, the cached exchanged token continued to be served until TTL expiry.
Summary
The token exchange cache (
authbridge/authlib/plugins/tokenexchange/cache/cache.go) keys onSHA-256(subjectToken || \0 || audience)and has no awareness of the JWTjticlaim. When a user token is revoked at Keycloak, cached exchanged tokens remain valid until their TTL expires (expires_in - 30s). This creates a revocation-blind window.This was discovered during a security review where a leaked user token continued to be honored by AuthBridge because the exchanged token was served from cache, bypassing any upstream revocation check.
Proposed Changes
1. Store
jtiin cache entriesAdd the subject token's
jticlaim to the cache entry so it can be checked against a revocation set on lookup:Set()gains ajtiparameter. The caller extracts thejtifrom the inbound subject token (standard JWT claim, included by Keycloak by default) before callingSet().2. Add a revocation set to the cache
New behavior:
Revoke(jti string, expiresAt time.Time)— adds thejtito the revoked set.expiresAtmatches the original token'sexpso entries self-clean (no point tracking revocation past token expiry).Get()— after finding a cache hit, checksrevoked[entry.jti]. If present and not yet expired, returns a miss (forces a fresh token exchange).evictExpired()— also sweeps expired revocation entries.3. Admin endpoint to trigger revocation
Add an HTTP endpoint on the ext-proc process (e.g.,
POST /admin/revokeaccepting{"jti": "...", "exp": ...}) so that an external actor — operator, incident response tooling, or a Keycloak event listener — can push revocations into the cache.Out of scope (follow-ups)
Motivation
Identified during a security review of an agentic AI deployment using AuthBridge for token exchange. A leaked user token was used by an AI agent, and even after the token was revoked in Keycloak, the cached exchanged token continued to be served until TTL expiry.