Token Revocation and Blacklisting
You log out. Your JWT is still valid. The server has no record it was ever issued. This is the stateless token revocation problem.
Why Revocation Is Hard#
JWTs are stateless by design. The server validates a token by checking the signature and expiry. It doesn’t consult a database. This is what makes them fast and scalable. But it means there’s no central list of “valid tokens” to update when a token should no longer be accepted.
If a user’s account is compromised and you rotate their password, any JWTs already issued remain valid until they expire. If your access token lifetime is 1 hour, that’s a 1-hour window after a breach where the attacker can still make authenticated requests.
The Token Blocklist#
The pragmatic fix: a Redis blocklist. When you revoke a token (on logout, password reset, or suspicious activity), store the token’s JTI (JWT ID claim, a unique identifier per token) in Redis with a TTL equal to the token’s remaining valid time. On every request, check the blocklist.
public boolean isRevoked(String jti, Instant expiry) {
return redisTemplate.hasKey("revoked:" + jti);
}
This adds one Redis lookup per request. At most auth service scales, that’s acceptable. The TTL means Redis self-cleans as tokens would have expired anyway.
Refresh Token Rotation#
The better long-term mechanism is short access token lifetime combined with refresh token rotation. Every time you use a refresh token, you get a new refresh token and the old one is invalidated. If an attacker steals a refresh token and uses it, the legitimate user’s next refresh attempt fails, signaling compromise.
At Salesforce#
A compromised integration user account had a token that was still making API calls 18 hours after we reset the password. The token had a 24-hour expiry, session invalidation only cleared the server-side session (which wasn’t used for token validation), and there was no blocklist. We added a blocklist keyed on integration user ID: any token issued before the credential reset timestamp was rejected. Not a per-JTI blocklist, but close enough and simpler to implement quickly.
What I’m Learning#
The stateless token revocation problem is the same trade-off as any distributed consistency problem: pick between fast and local (no revocation check) versus correct and coordinated (blocklist lookup). Short token lifetime is the closest thing to having both.
What’s the worst breach window you’ve had between credential rotation and token expiry?