rossoctl / rossoctl/cortex

Add JTI-based revocation to token exchange cache

Open
#758 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
13
Forks
40
Avg merge
12h 17m
Merged PRs (30d)
156

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with authbridge/authlib/plugins/tokenexchange/cache/cache.go and trace the cache Set/Get callers to understand how subject-token claims and expiry are handled. Then inspect the ext-proc HTTP entry points for the admin endpoint; done means revoked JTIs cause cache misses, expired revocations are removed, and the endpoint can submit a JTI and expiry.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
authentication, backend, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
40/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.