The Problem with Short-Lived JWTs
JWTs cannot be revoked before they expire — a fundamental property of stateless tokens. This forces a choice: long-lived tokens (security risk if stolen) or short-lived tokens (users get logged out constantly). Refresh tokens solve this tension.
How Refresh Tokens Work
- Login: Server issues a short-lived JWT (access token, 15 minutes) and a long-lived refresh token (7-30 days) stored in an httpOnly cookie.
- Normal requests: Client sends JWT in Authorization header. Server verifies without database lookup.
- When JWT expires: Client sends refresh token to a dedicated endpoint. Server checks it exists and has not been revoked, then issues a new JWT.
Refresh Token Rotation
The most secure pattern rotates the refresh token on every use — each refresh call returns a new refresh token and invalidates the old one. This detects theft: if an attacker uses a stolen token after the legitimate user already used it, the server sees a previously-used token and can invalidate the entire session.
// Rotation pattern (pseudocode)
POST /auth/refresh
Server:
1. Look up token in refresh_tokens table
2. If not found or already used: reject + log suspicious activity
3. Mark old token as used
4. Issue new refresh token + new JWT
5. Return both to client
Storage Rules
- Refresh tokens — httpOnly, Secure, SameSite=Strict cookie (not readable by JavaScript)
- Access JWT — memory or httpOnly cookie — never localStorage
- Never store both in localStorage — a single XSS vulnerability gives an attacker unlimited session access
Revocation
Unlike JWTs, refresh tokens live in a database table, so revocation is instant: delete the row and the user is logged out on their next refresh cycle. This is how "log out all devices" works — delete all refresh tokens for that user ID.