JWT and Token Rotation
JWT and Token Rotation
JWT is a lightweight way to carry claims, but it does not by itself answer every question an authentication system needs. We cover the short-access-token / long-refresh-token pattern, invalidation strategies, algorithm choice, and a well-known pitfall (alg:none).
1. About JWT
JWT (JSON Web Token, RFC 7519, 2015) has three parts:
header . payload . signature
- header — algorithm and token type (
{"alg":"HS256","typ":"JWT"}). - payload — claims (
sub,exp,iat,iss,aud, plus custom). - signature — signs the header + payload. HMAC or asymmetric signature.
Each part is base64url encoded; the signature takes the encoded strings as input, not the decoded values.
Standard claims:
| Claim | Meaning |
|---|---|
iss |
Issuer |
sub |
Subject (user ID) |
aud |
Audience |
exp |
Expiration (Unix epoch) |
nbf |
Not-before |
iat |
Issued-at |
jti |
Token unique ID |
2. Algorithms
| Algorithm | Kind | Notes |
|---|---|---|
| HS256 / HS384 / HS512 | HMAC + SHA-2 | Symmetric key. Issuer and verifier share the secret. |
| RS256 / RS384 / RS512 | RSA + SHA-2 | Asymmetric. Sign with private key, verify with public. 2048+ recommended. |
| ES256 / ES384 / ES512 | ECDSA | Asymmetric. Smaller signature size. Parameters vary by curve. |
| EdDSA (Ed25519) | Edwards-curve signature | Fast and deterministic. RFC 8037. |
How to choose:
- A single service issues and verifies internally — HS256 is often enough.
- Other services need to verify (distributed) — go asymmetric (RSA, EC, Ed25519). Distribute only the public key.
- OIDC IdP integration — RS256 or ES256 is typically the standard.
3. Access vs Refresh Separation
Two requirements collide if a single JWT handles session management:
- Long sessions — users should not have to log in every time.
- Short expiry — limit the blast radius if a token is stolen.
The fix is to separate access and refresh tokens:
- Access Token — short TTL (5–15 min). Sent with resource calls. Discarded after expiry.
- Refresh Token — long TTL (7–30 days). Used to mint new access tokens after expiry. Where it is stored (httpOnly cookie, secure storage) matters.
4. Refresh Token Rotation
When the refresh token is used once, the user gets a fresh refresh token and the old one is invalidated. If a stolen token is used, the next rotation by the legitimate user fails — and that failure detects the breach.
The server records the refresh token's jti and user ID in Redis or the DB:
on refresh:
if redis.get("rt:" + jti) != "valid":
revoke all sessions for user
return 401
redis.del("rt:" + jti)
new_rt = issue_new()
redis.set("rt:" + new_rt.jti, "valid", ttl=30d)
return new_access, new_rt
Seeing the old jti again is treated as "reuse detection" and all of that user's sessions are killed.
5. Invalidation Strategies
JWT statelessness is a strength, but invalidation is hard.
Block-list (Deny-list) — record the jti of revoked tokens in Redis with a short TTL. Verification checks the block-list. Keep entries only for the access token's TTL.
key = "bl:jwt:" + jti
ttl = exp - now + clock_skew
Pros — simple. Cons — an external lookup per call.
Allow-list — track every issued token and check on every call. Strong control, but stateless JWT loses much of its meaning. It effectively becomes a session token.
Short TTL + Refresh Rotation — the most common compromise. The access token has no built-in invalidation; a short TTL shrinks the risk window, and refresh rotation plus reuse detection handles the rest.
Clock skew — when checking exp, absorb the difference between server and issuer clocks. A 30–60 second margin is typical. Too large a margin lengthens the post-expiry usability window.
6. alg:none and BCP
The JWT spec defines alg:none, but that value is the starting point of verification bypass (the JWT none-algorithm vulnerability, reported in 2015). Libraries are recommended to reject alg:none by default, and RFC 8725 (JSON Web Token Best Current Practices, 2020) consolidates the guidance.
Key recommendations:
- Make
algan explicit allow-list (['HS256','RS256']) — do not let the library accept the algorithm straight from the token header. - Block key-confusion attacks — prevent passing an RS256 public key to an HS256 verification function.
- Validate
kid— never use the key ID directly as a file path. - Separate authentication and encryption (JWS, JWE).
7. Where to Store Tokens
- httpOnly Secure cookie — protected from XSS. Needs CSRF protection (SameSite + token).
- JS-accessible storage (localStorage) — exposes the token if XSS occurs. Not recommended.
- Memory (app-instance global) — vanishes on new tab or refresh.
For the web, httpOnly cookie + SameSite=Lax or Strict + a short access TTL is common.
8. Key Management
- A key rotation schedule — multiple keys served from a JWKS endpoint.
- Issuance writes the key identifier to the
kidheader. - Verification caches JWKS and handles expiry.
Asymmetric private keys go into a secret manager (KMS, Vault, 1Password).
9. Claim Design
Minimal information — do not put email or sensitive data into the payload (JWT is signed, not encrypted).
Permission snapshots — embedding permissions in the token means user permission changes do not take effect immediately. Compensate with a short TTL or check permissions against the DB on every call.
10. Common pitfalls
It is not encrypted — JWT is only signed. Anyone can decode the payload. Sensitive data belongs in JWE or a server-side session.
Trusting the alg header — letting the token declare its own algorithm is an attack surface. Enforce an allow-list.
Weak HS256 secrets — short secrets are vulnerable to brute force. Use 32+ bytes of random data.
Missing exp validation — do not trust library defaults; unit-test it.
Single-use refresh not enforced — the rotation pattern loses meaning. Invalidate on use, immediately.
What logout means — the token is alive for the access TTL. A trade-off between UX and safety.
Multi-device invalidation — decide policy: should a password change on one device end sessions on others?
Closing thoughts
JWT is a lightweight answer for stateless authentication, but invalidation, refresh, and key rotation each bring operational weight. Operational safety improves substantially when short access TTLs, refresh token rotation, and an alg allow-list all run together. Sensitive information belongs in a server-side session, not the payload.
Next
- oauth-state-pkce
- rate-limit-redis
We refer to RFC 7519 JWT, RFC 8725 JWT BCP, RFC 7515 JWS, RFC 7516 JWE, OWASP JWT Cheat Sheet, Auth0 Refresh Token Rotation, JWT.io, and RFC 9700 OAuth 2.0 BCP.