Refresh Tokens
Refresh Tokens
In the previous lessons you built a system that issues a JWT access token on login and validates it on every protected request. That works well, but it contains a hidden trade-off: for the token to be useful without a round-trip to the database on every request it must be stateless, which means the server cannot revoke it before it expires. Short-lived tokens (5–15 minutes) minimise the revocation window, but they also mean the user must re-login every few minutes — terrible UX.
Refresh tokens solve this tension. The idea is simple:
- Issue a short-lived access token (5–15 minutes) — the bearer credential for API calls.
- Issue a long-lived refresh token (days to weeks) — stored securely, used only to obtain new access tokens.
- When the access token expires, the client presents its refresh token to a dedicated
/auth/refreshendpoint and receives a fresh access token without re-prompting for credentials.
The Refresh Flow Step by Step
- User logs in. Server returns
{ accessToken, refreshToken }. The refresh token is persisted in the database and returned to the client. - Client stores the access token in memory (never
localStoragefor high-security apps) and the refresh token in anHttpOnlycookie or secure storage. - Client sends requests with the access token in the
Authorization: Bearer <token>header. - When the server returns
401 Unauthorized(access token expired), the client callsPOST /auth/refreshwith the refresh token. - Server validates the refresh token against the database (checks it exists, has not been revoked, has not expired). If valid, it issues a new access token (and optionally rotates the refresh token).
- Client retries the original request with the new access token.
Persisting Refresh Tokens
Unlike access tokens, refresh tokens are stateful. You must store them in the database so you can revoke them. A minimal entity looks like this:
UUID.randomUUID().toString() or SecureRandom bytes encoded as hex) lets you delete or mark it revoked in the database instantly.
The RefreshTokenService
The Auth Controller — Login and Refresh Endpoints
Refresh Token Rotation and Reuse Detection
Notice the call to createFor(user) inside the /auth/refresh endpoint. This is refresh token rotation: every successful refresh issues a brand-new refresh token and revokes the old one. This is a critical security technique.
Why does rotation matter? If an attacker steals a refresh token and uses it, token rotation means the legitimate user's token is revoked at the same moment. The next time the legitimate user tries to refresh, validation will fail — alerting you (and potentially them) that the token was compromised. Without rotation, a stolen refresh token is valid until its natural expiry date.
Access Token Lifetime Recommendations
These are practical starting points — adjust based on your threat model:
- Access token: 5–15 minutes. Stateless, validated in-memory. Short window limits stolen-token exposure.
- Refresh token: 7–30 days. Stateful, database-backed, rotated on use. The
expiresAtcolumn in the DB is your hard deadline. - Absolute session limit: Set a maximum total session duration (e.g. 90 days). After that, the user must re-authenticate regardless of token activity. This prevents a single login session from living forever on a shared device.
Storing Tokens Safely on the Client
The server's token design only matters if the client stores tokens correctly:
- Access token: keep in JavaScript memory (a module-level variable or state store). Never in
localStorageorsessionStorage— XSS can read those. - Refresh token: send in an
HttpOnly; Secure; SameSite=Strictcookie.HttpOnlymakes it invisible to JavaScript;SameSite=Strictprevents CSRF. The server's/auth/refreshendpoint reads it from the cookie rather than the request body in this stricter variant.
Summary
Refresh tokens are the mechanism that lets you have both short-lived, hard-to-misuse access tokens and a smooth user experience. Access tokens stay stateless and fast; refresh tokens are stateful and revocable. Pair them with rotation to detect theft, store them server-side in a database, and combine HttpOnly cookies on the client side for the best security posture. In the next lesson you will look at OAuth2 and OpenID Connect — the industry-standard protocols that formalise exactly this kind of token lifecycle.