JWT Best Practices: Short Expiry, Refresh Rotation, and Revocation Done Right
JSON Web Tokens are everywhere — and so are the mistakes people make with them. A practical guide to using JWTs safely in production.
JWTs (JSON Web Tokens) are the default stateless authentication mechanism for modern APIs. They're also one of the most commonly misused pieces of infrastructure in production systems.
Here's how to use them correctly.
The fundamental trade-off
JWTs are stateless: the server validates them by checking the signature, not by looking them up in a database. This is great for scale and terrible for revocation. Once issued, a JWT is valid until it expires — there is no server-side "log out" without extra machinery.
This trade-off drives every best practice below.
1. Keep access tokens short-lived
The single most important rule. An access token should expire in minutes, not hours. 15 minutes is a sensible default. If an access token leaks, the window of abuse is small.
access token TTL: 15 min
refresh token TTL: 7-30 days
Long-lived access tokens are the #1 JWT mistake. If your access token expires in 30 days, you've built a stateful session system with extra steps and no revocation.
2. Use refresh tokens with rotation
A refresh token is longer-lived but only used to mint new access tokens — never sent on every request. Rotate it on every use: each refresh issues a new refresh token and invalidates the old one.
This detects token theft: if an attacker uses a stolen refresh token, the legitimate user's next refresh fails (their token was just invalidated), and you can lock down the session family.
3. Implement revocation despite statelessness
Stateless JWTs can't be revoked by definition, but you can layer revocation on top:
- A denylist of token IDs (jti) for the rare cases you must revoke (compromised account, password change). Check it on every request. Yes, this makes you stateful — only for revoked tokens, which is a small set.
- A token version counter on the user record. Bump it on password change / logout-all. Reject tokens whose version doesn't match. Cheap and effective.
4. Store tokens safely on the client
- Web apps: HttpOnly, Secure, SameSite=Lax cookies. Not localStorage — XSS can read it.
- Mobile: the platform secure storage (Keychain on iOS, Keystore on Android).
- Never put a JWT in a URL query parameter — it leaks into logs, Referer headers, and browser history.
5. Validate everything
A JWT is not "logged in" just because it exists. Validate:
- Signature with the correct algorithm (and reject
alg: none— a classic attack). - exp (not expired).
- iss (issuer matches expected).
- aud (audience matches your API).
- iat / nbf sanity (issued in the past, valid-after in the past).
Skipping any of these is a vulnerability. Use a well-maintained library, not hand-rolled base64 decode.
6. Don't put sensitive data in the payload
The JWT payload is base64, not encrypted. Anyone who intercepts the token can read it. Put only the user ID and minimal claims inside. Never passwords, never secrets, never PII you wouldn't put in a log file.
7. Pick the right signing algorithm
- RS256 / EdDSA (asymmetric) for multi-service setups where many services verify but only one signs.
- HS256 (symmetric) is fine for single-service apps but requires the secret on every verifying service, which widens the blast radius of a leak.
Avoid none entirely. Pin the expected algorithm in your verifier — never trust the alg header.
8. Set clocks correctly
JWT validation depends on timestamps. If your server clock drifts, tokens fail or — worse — expired tokens validate. Use NTP. Give a small leeway (leeway: 30s) for clock skew between issuer and verifier.
Putting it together
A sane production setup:
- 15-minute access tokens in HttpOnly cookies
- 7-day rotating refresh tokens in HttpOnly cookies, with reuse detection
- A
jtidenylist for forced revocation - RS256 signing with keys rotated quarterly
- Full validation (sig, exp, iss, aud) on every protected request
This isn't over-engineering — it's the minimum for "auth done right," which is exactly what the ShipReady Auth & Access Control checklist verifies. Run it before you ship.
Run the full checklist
Put this into practice — generate a stack-specific production readiness checklist and track your progress, free.
Open the checklistGet new articles in your inbox
Practical production readiness guides. No spam, unsubscribe anytime.
Join developers shipping safer. We email ~2× per month.
Keep reading
What Is a Zip Bomb? The 42KB File That Crashes Servers (DoS Explained)
A 42 KB file that expands to 4.5 petabytes. Here's how zip-bomb denial-of-service attacks work, the real numbers behind the famous 42.zip, and how to defend against them.
The Production Readiness Checklist Every Indie Hacker Should Run Before Launch
Before you ship your SaaS to real users, run through these 12 categories. The 80/20 of what actually matters — and what you can safely skip on day one.
12 Security Headers You Must Set Before Going to Production
CSP, HSTS, X-Frame-Options and the rest. A plain-English reference for every HTTP security header your web app should set before launch.