Zero-Trust API Security: Short-Lived JWTs, Refresh Token Rotation, and Revocation Lists
Hardening stateless token authentication against replay attacks and token theft in modern web applications.
1. The Problem with Long-Lived Access Tokens
A JWT with a 24-hour or 7-day expiry is a bearer token: whoever holds the string is treated as the user, full stop. There's no binding to a device, a session, or an IP range by default, so once a token leaks - through an XSS payload, a misconfigured logging pipeline, a browser extension reading localStorage, or a proxy that logs request headers - it stays valid for as long as its exp claim allows. The attacker doesn't need your password, they don't need to touch your auth server again, they just replay the token until it expires.
The instinct to fix this by making tokens last longer 'for a better user experience' is exactly backwards. Every hour of extra lifetime is an hour of extra blast radius for every token that leaks. The right lever is the opposite: shrink the access token's lifetime to the point where a leaked token is nearly worthless on its own, and move the actual session continuity into a separate mechanism that can be revoked and monitored.
This is the core move of a zero-trust posture applied to token auth: never trust a credential just because it was valid a minute ago. Every request re-derives trust from a token that is short-lived enough that 'was this ever stolen' becomes a much smaller window to worry about, typically 5 to 15 minutes for access tokens serving browser or mobile clients.
2. Splitting Access and Refresh Tokens
The standard pattern is a pair: a short-lived, stateless access JWT (5-15 minutes) sent with every API call, and a long-lived, stateful refresh token (days to weeks) used only to mint new access tokens. The access token is verified with a signature check and never touches the database - that's the whole point of using a JWT there, it's fast and horizontally scalable. The refresh token, by contrast, is looked up in a store on every use, because it needs to be revocable and because reuse needs to be detectable.
This split matters because it lets you optimize two different things independently. Access tokens optimize for verification speed and statelessness on the hot path (every API call). Refresh tokens optimize for control - you want to know exactly which refresh tokens exist, when they were last used, from what device, and you want the ability to kill one instantly without touching any other session.
A common mistake is putting sensitive authorization data (roles, permissions, tenant ID) only in the access token and treating the refresh token as a dumb opaque string. That's correct for the refresh token's payload, but it means every privilege change (a role downgrade, an account suspension) only takes effect once the current access token expires. If your access tokens live 15 minutes, that's an acceptable window; if they live 24 hours, it's not.
3. Refresh Token Rotation with Reuse Detection
A static refresh token that gets reused indefinitely is still a long-lived bearer credential, just one layer removed - if it leaks, the attacker can mint access tokens forever. Rotation fixes this: every time a refresh token is used to get a new access token, the server also issues a brand new refresh token and immediately invalidates the old one. The client swaps its stored refresh token for the new one atomically. A given refresh token is now single-use by design.

