Skip to content

Add JTI-based revocation to token exchange cache #758

Description

@usize

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    New/ToDo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions