A JWT is stateless, so how do you revoke one before its exp time arrives?
answer
- Signatures cannot be taken back
- Shrink the window instead of closing it
- One identifier per token, kept until it expires
- One timestamp per user kills all their tokens
- Rotating the key is the sledgehammer
basics
~20 sYou cannot un-sign a token, so you add state on the verifier side: short access-token lifetimes with revocable refresh tokens, a jti denylist consulted during validation, or a per-user cutoff timestamp compared against iat. Each trades statelessness for revocation latency.
solid answer
~40 sNothing can withdraw a signature already given, so revocation is always something the *verifier* consults. Three mechanisms, usually combined. **Short lifetimes**: make access tokens live minutes, and hold the real session in a long-lived refresh token that lives in the issuer's database and can be deleted — revocation latency is then bounded by the access token's TTL. **A `jti` denylist**: every token carries a unique `jti`, and the verifier checks a shared store of revoked identifiers; entries only need to be kept until each token's own `exp` passes. **A per-principal cutoff**: store `tokensValidAfter` for each user and reject any token whose `iat` is earlier, which kills every outstanding token at once on password change or account disable. Rotating the signing key revokes everything globally and is a break-glass action, not a routine one.
code
json · 8 lines{
"iss": "https://auth.example.com/",
"sub": "user-1042",
"aud": "orders-api",
"jti": "9f1c2b7e-3c1a-4f0d-9b2e-77a1c0a5e001",
"iat": 1764496400,
"exp": 1764496700
}go deeper
Know that a signed token stays valid until it expires and that short lifetimes plus a refresh token are the usual answer. Be able to say why deleting something server-side does not reach an already-issued token.
Explain the three mechanisms and their state: refresh-token storage at the issuer, a jti denylist with per-entry TTL, and a per-user cutoff compared against iat.
Show the operational judgment — pick the mechanism from the required revocation latency, handle denylist unavailability explicitly, and keep key rotation as break-glass rather than a logout tool.
Own the tradeoff at platform level: state a target revocation latency, choose the cheapest mechanism that meets it, and be explicit that immediate revocation and pure statelessness cannot both be true.
## The problem stated precisely A signed token is a statement frozen at minting time: *this issuer asserted these claims and they hold until `exp`*. A verifier that checks only the signature and the claims is, by construction, unable to learn that anything changed afterwards — a user logged out, an employee was terminated, a token was stolen, a role was removed. That is the price paid for validating without calling the issuer. So the honest answer is never "you can revoke a JWT"; it is "you reintroduce exactly as much state as your revocation requirement demands, and no more". ## Mechanism 1 — short lifetimes plus a revocable refresh token The standard shape. The access token presented to services is deliberately short-lived (commonly a few minutes). A separate refresh token, held by the client and *recorded in the issuer's storage*, is exchanged for new access tokens. Revocation becomes a database delete at the issuer: the refresh token stops working immediately, and the last outstanding access token dies naturally at its `exp`. This keeps resource servers completely stateless — they still just verify and check claims — and moves all state to one place that already has a database. The cost is a bounded window: a stolen access token remains usable for up to its remaining lifetime. Shrinking the TTL shrinks the window but increases refresh traffic against the issuer, so the TTL is a tuning dial between revocation latency and issuer load. Refresh-token rotation strengthens this: each refresh issues a new refresh token and invalidates the old one, so replay of a stolen refresh token collides with the legitimate client's use and can be detected as reuse, triggering invalidation of the whole chain. ## Mechanism 2 — a jti denylist Give every token a `jti`, a unique identifier, and have verifiers consult a shared store of revoked identifiers as part of validation. Revoking a specific token is then a write to that store. The practical points that separate a good answer from a hand-wave: - **The store is bounded.** An entry only has to survive until the token's own `exp`, so entries carry a TTL and the set stays proportional to *revoked tokens still within their lifetime*, not to all tokens ever issued. - **It is a lookup on the hot path.** Every request now depends on a cache or store, so latency and availability of that store become authentication concerns. Deployments typically use an in-memory data store with local caching and short TTLs. - **Fail-closed or fail-open is a real decision.** If the denylist is unreachable, accepting tokens keeps the service up but temporarily un-revokes everything; rejecting them fails securely but converts a cache outage into an outage. For most systems, high-value operations fail closed and read-only paths may fail open — but that must be a decision, not an accident. A denylist is precise (kill one token) which makes it the right tool for "this device was lost" and the wrong tool for "disable this account". ## Mechanism 3 — a per-principal cutoff Store, per user, a timestamp such as `tokensValidAfter`, updated whenever their credentials or status change. Validation compares the token's `iat` against that value and rejects anything issued earlier. One write invalidates every outstanding token for that user — logout-everywhere, password change, account disable, role revocation — without tracking individual tokens. The lookup is per user rather than per token, which caches well, and the stored data is one small field per principal. The limitation is granularity: it cannot revoke one session while leaving another alive, so it complements rather than replaces a denylist. ## Mechanism 4 — key rotation as break-glass Retiring a signing key invalidates every token signed with it, everywhere, instantly. That is the correct response to a suspected key compromise and a terrible response to one user's logout: every active session in the system ends at once. Keep it in the runbook, not in the request path. ## Choosing, and defending the choice Start from the requirement: *how quickly must access actually stop?* If the answer is "within a few minutes", short lifetimes alone satisfy it, and you keep full statelessness. If the answer is "immediately, for high-value operations", you need a lookup, and the cheapest sufficient one is usually the per-user cutoff, with a `jti` denylist added when individual-session revocation is genuinely required. Be explicit about what you have given up: every revocation mechanism is state on the verification path, which is precisely the property that made the token attractive. A design that claims both perfect statelessness and immediate revocation is describing something else. ## Related failure to name Authorization changes have the same shape as revocation. A role removed at 10:00 is still asserted by a token minted at 09:58, so permissions-in-the-token are permissions-as-of-issue-time. Systems that need instant permission changes either keep lifetimes very short or look up authorization data at request time rather than trusting claims.
- How large does a jti denylist actually get, and how do you keep it bounded?Only revoked tokens that have not yet reached their own `exp` need entries, so you store each `jti` with a TTL equal to its remaining lifetime and let the store expire it. With short access-token lifetimes the set stays small — proportional to recent revocations, not to issued tokens. Unbounded growth only happens if you forget the TTL.
- Your denylist store becomes unavailable. Do you accept or reject tokens?Decide it deliberately and per operation. Failing closed is secure but turns a cache outage into an authentication outage; failing open keeps traffic flowing while temporarily un-revoking everything. A common split is to fail closed on state-changing or high-value endpoints and degrade gracefully elsewhere, with alerting so the window is short and visible.
- Why not just make access tokens live thirty seconds and skip revocation state entirely?It works, and it is genuinely the cleanest design when the issuer can take the load. The cost is a refresh round-trip roughly every thirty seconds per active client, which concentrates traffic and availability risk on the issuer, and it amplifies clock-skew sensitivity because the token's whole life is close to the tolerance window.
- A user's admin role is removed. Does revocation solve that?Only if you revoke and force re-issue. Permissions carried as claims are true as of minting, so the old token keeps asserting the role until it expires. Either keep lifetimes short enough that stale permissions are acceptable, bump the per-user cutoff so existing tokens are rejected, or resolve authorization at request time instead of trusting the claim.
saying these in an interview costs you the question
- Claims a JWT can be deleted or invalidated at the issuer
- Proposes a denylist with no expiry, growing forever
- Rotates the signing key to log one user out
- Says revocation is impossible so nothing can be done
- Ignores that a denylist lookup makes verification stateful