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.
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).
| Tier | How | Latency | Coupling |
|---|---|---|---|
| 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.
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
}
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
)
Verify only ever adds a rejection — it never grants on the feed’s say-so, and all the normal signature / issuer / audience / expiry checks still run first. Token expiry is the always-present backstop.revoked_at IS NOT NULL AND expires_at > now(), so expired tokens fall out automatically. Set size is bounded by revoke-rate × max TTL.keysByKID map the verifier already uses.ver. The SDK rejects a regressing version and keeps its current snapshot; the JWS also requires iss and exp.WithFeedFailClosed, a stale or unreachable feed reverts to TTL-bounded revocation rather than rejecting valid tokens.LEGANT_TOKEN_EXCHANGE_ACCESS_TOKEN_LIFESPAN enlarges the Tier-C backstop. Tokens are clamped to min(now+ttl, grant expiry), so a token never outlives its delegation grant.act/constraint checks.