Access tokens should be short-lived, but users don't want to log in every five minutes. Here's how refresh tokens close that gap safely.
The problem refresh tokens solve
Short-lived access tokens are a core security best practice — if one leaks, the damage window is small. But nobody wants to re-enter credentials every 10 minutes. **Refresh tokens** let a client silently obtain a new access token without the user noticing.
How the flow works
1. The user authenticates once and receives two tokens: a short-lived **access token** (a JWT, typically 5–15 minutes) and a longer-lived **refresh token** (hours to weeks).
2. The client sends the access token on every API request.
3. When the access token expires, the client sends the refresh token to a dedicated endpoint like `/auth/refresh`.
4. The server validates the refresh token, issues a brand-new access token (and often a new refresh token), and the cycle continues.
Refresh tokens should not be JWTs (usually)
This surprises people. Access tokens benefit from being JWTs because they're verified frequently and statelessly. Refresh tokens are verified rarely (once per refresh cycle), so there's little upside to making them self-contained — and real downside, because a stateless refresh token can't be revoked without a blocklist. Most production systems use an **opaque, random, database-backed** refresh token instead, so it can be looked up, revoked, and rotated server-side.
Refresh token rotation
Every time a refresh token is used, issue a new one and invalidate the old one immediately. This is called **rotation**, and it turns refresh token theft into a detectable event: if a stolen, already-used refresh token is replayed, the server can recognize the reuse, invalidate the entire token family, and force re-authentication.
Storage matters
- Store the refresh token in an **HttpOnly, Secure, SameSite cookie** if your architecture allows it — this keeps it out of reach of JavaScript entirely.
- If you must store it in a mobile app, use the platform keychain/keystore, not plain storage.
- Never put a refresh token in `localStorage` if you can avoid it — see our dedicated post on storage tradeoffs.
A minimal Express-style refresh endpoint
- Look up the refresh token in your database by its hashed value (never store it in plaintext).
- Confirm it hasn't been revoked and hasn't expired.
- Issue a new access token JWT with a fresh `exp` claim.
- Rotate the refresh token: delete the old record, insert a new one, return the new value to the client.
- If the incoming refresh token was already marked "used," treat this as a reuse/theft signal and revoke the whole session family.
Common mistakes
- Making refresh tokens live forever with no expiration at all.
- Reusing the same refresh token indefinitely instead of rotating it.
- Storing refresh tokens in plaintext in your database — hash them like passwords.
- Not tying refresh tokens to a specific device/session, making theft harder to detect.
Checklist
- Access tokens: short-lived JWTs, 5–15 minutes.
- Refresh tokens: long-lived, opaque, stored server-side, hashed at rest.
- Rotate refresh tokens on every use and detect reuse.
- Bind refresh tokens to a session or device where possible.
- Provide a clear "log out everywhere" action that revokes all refresh tokens for a user.