Refresh tokens and rotation

JWT Authentication Course · lesson 10 of 13 · 6 min read

Short access tokens are only safe if refresh is designed properly.

Open this lesson in the learning hub

Key points

  • The pattern is a short access token - minutes - plus a long refresh token used only against the token endpoint. A leaked access token then expires quickly on its own.
  • That only works if the refresh token is protected better than the access token. It is the more valuable credential, because it mints new access tokens.
  • Rotation: each refresh returns a new refresh token and invalidates the old one. A token used twice is evidence of theft, because the legitimate client would have discarded it.
  • On detecting reuse, revoke the whole family of tokens descended from that grant. That turns a stolen refresh token into one extra request rather than indefinite access.
  • Refresh tokens must be stored server-side to be revocable - which is fine, because there are far fewer of them than requests, so the lookup cost is negligible.
  • A public client such as a browser or mobile app cannot keep a secret, so its refresh token must be rotated and bound to the client. For browsers, an httpOnly cookie holding the refresh token beats localStorage.

Example

# THE FLOW
#
#   POST /token  grant_type=authorization_code
#     -> access_token  (exp 15 min)
#     -> refresh_token (exp 30 days, family F1, generation 1)
#
#   ... 15 minutes later ...
#
#   POST /token  grant_type=refresh_token&refresh_token=<gen 1>
#     -> new access_token
#     -> new refresh_token (family F1, generation 2)
#     -> generation 1 is now INVALID

---
# REUSE DETECTION - the point of rotation.
#
#   attacker steals generation 2 and uses it   -> gets generation 3
#   legitimate client later uses generation 2  -> ALREADY USED
#     -> revoke the ENTIRE family F1
#     -> both parties are logged out; the user re-authenticates
#
# Without rotation the attacker refreshes silently for 30 days and
# nothing ever detects it.

---
-- What the server actually stores. Small table, cheap lookups.
CREATE TABLE refresh_token (
    id           UUID PRIMARY KEY,
    family_id    UUID        NOT NULL,   -- all tokens from one login
    user_id      BIGINT      NOT NULL,
    token_hash   CHAR(64)    NOT NULL,   -- store a HASH, never the token
    generation   INT         NOT NULL,
    used_at      TIMESTAMP   NULL,       -- non-null + presented = THEFT
    expires_at   TIMESTAMP   NOT NULL,
    revoked      BOOLEAN     NOT NULL DEFAULT FALSE,
    UNIQUE (token_hash),
    INDEX idx_family (family_id)
);

-- On reuse:
UPDATE refresh_token SET revoked = TRUE WHERE family_id = ?;

# Browser storage, ranked:
#   httpOnly + Secure + SameSite cookie   best - JavaScript cannot read it
#   in-memory (a JS variable)             good - lost on refresh, by design
#   localStorage                          worst - any XSS reads it

Rotate refresh tokens and revoke the whole family on reuse - that is what converts theft from indefinite access into one extra request.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the JWT Authentication Course course, and every lesson in it is listed on the JWT Authentication Course contents page.