Legant's revocation design: three tiers — per-call store check, a signed offline feed, and a TTL backstop — with the worst case never worse than the token's short TTL.

Revocation

How to kill a token that’s already signed and in the wild.

Most agent-auth pitches promise “instant offline revocation,” which is physically impossible without a per-call round-trip. Legant gives you three real tiers and lets you pick the coupling you want — and the worst case is never worse than the token’s short TTL.

The tension

A delegation token is a self-contained, RS256-signed JWT. By construction it is valid until it expires — any holder of the issuer’s public key can verify it with no callback to Legant. That’s the whole point of offline authorization. But it’s also the problem: once minted, the token is unkillable by signature alone until its exp passes.

Offline enforcement and instant revocation are only in tension if one verifier must be both. Legant doesn’t pretend a single mode squares that circle — it offers three tiers and lets you choose per verifier.

A token id (jti) is recorded for every minted delegation token, and revocation is a denylist over those jtis — surfaced synchronously (Tier A), as a signed offline feed (Tier B), or not at all beyond the token’s own short TTL (Tier C).

The three tiers

TierHowLatencyCoupling
A per-call store check The MCP gateway and RFC 7662 introspection query the Postgres revocation store for the token’s jti on every call. Active only if the row exists, revoked_at IS NULL, and exp hasn’t passed. Unknown jti fails closed. Immediate — the next call after the revoke commits is rejected. Tightest: every verified call needs the issuer’s DB reachable; a store error is treated as not-active (reject).
B signed feed
/.well-known/revoked
The issuer publishes a JWS-signed snapshot of revoked-but-unexpired jtis, signed with the same JWKS key (no new trust root) and stamped with a monotonic version. The SDK polls it on a timer and checks an in-memory set — no per-request callback. Within your poll interval; ≤5s server cache; never later than token expiry. Loose: fully offline at request time; the resource server only reaches the feed URL on its own polling timer.
C TTL backstop With no feed configured, the SDK validates signature / iss / aud / exp and the act claim; revocation is bounded by the token’s short TTL alone. This is also the default fail-open behavior when a feed is stale (fail-closed is opt-in). Up to the token’s remaining TTL — it simply expires. Zero: no DB, no feed, fully offline.

Tier A and Tier B read the same exchanged_tokens table, so they’re consistent by construction: the feed is just an offline projection of the per-call denylist.

The endpoint

GET /.well-known/revoked
Content-Type: application/jwt
Cache-Control: public, max-age=5

The body is a single RS256-signed compact JWS whose claims are:

{
  "iss":  "https://issuer.example",
  "iat":  1718900000,
  "exp":  1718900060,        // iat + feedTTL (1m)
  "ver":  42,                // monotonic int64; verifiers reject a regress
  "jtis": ["...", "..."]     // sorted list of revoked, unexpired token ids
}

Tier B in two lines

The resource server fetches the feed once, polls it in the background, and hands it to the verifier. No per-request callback after this.

// keysByKID is the same JWKS map the Verifier uses — no new trust root.
feed, err := sdk.FetchRevocationFeed(ctx, issuer+"/.well-known/revoked", issuer, keysByKID)
feed.StartPolling(ctx, 10*time.Second, func(err error) { log.Print(err) }) // non-fatal

v := sdk.NewVerifier(issuer, audience, keysByKID, sdk.WithRevocationFeed(feed))
claims, err := v.Verify(token) // offline; returns sdk.ErrRevoked if the jti is in the feed

To couple availability to freshness, opt in to fail-closed — Verify then rejects when the feed is staler than the bound:

v := sdk.NewVerifier(issuer, audience, keysByKID,
    sdk.WithRevocationFeed(feed),
    sdk.WithFeedFailClosed(30*time.Second), // default without this is fail-open-to-TTL
)

Safety properties

What is not claimed