Refresh-token rotation and token revocation

Each refresh burns the old token for a new one, so a replayed copy trips the alarm and the whole session family gets killed.

kill the stolen tokenmake old tokens stop workinglog out of all devicesone time use refresh tokenrefresh token reuse detectiontoken rotatondetect a stolen sessioninvalidate the token after logoutwhy do my users get randomly logged out

See it

Live demo coming soon

What it is

Rotation makes refresh tokens single use. Every time a client redeems one, the server hands back a new refresh token and burns the old one. The payoff is reuse detection: if an already-spent token shows up again, either the legitimate client replayed it or a thief is using a copy, and you cannot tell which. So you revoke the entire token family descended from that original login and force a fresh sign-in.

Be precise about what that buys you, because rotation is often oversold. It gives you a signal, not a guarantee. Detection only fires once two competing copies of the same token are actually presented. If a thief redeems the stolen token first and the real client never comes back with the copied ancestor, nothing collides and the attacker can keep rotating the family quietly. That is why the OAuth 2.0 security best current practice treats rotation as one option for public clients and sender-constrained refresh tokens (mTLS or DPoP, bound to a key the thief does not have) as the other, and why you pair either with bounded absolute lifetimes, inactivity expiry, and revocation that a human can actually trigger.

Revocation is the manual switch. Keep refresh tokens (or their hashes) as rows you can delete, one per device or session, and wire the obvious triggers to them: sign out, sign out everywhere, password change, MFA reset, role downgrade, admin disabling an account. RFC 7009 specifies a /revoke endpoint if you are speaking real OAuth. Store enough context per row (device label, IP, last used) that a user can look at a session list and recognize what to kill.

Gotcha, and it is the one that generates support tickets: rotation plus unreliable networks equals false-positive logouts. Two browser tabs refreshing at once, or a response lost after the server already rotated, both look exactly like reuse. The first fix is on the client: serialize refreshes through one shared in-flight promise so tabs and queued requests never race. If you also need to survive a lost response, do it with an idempotency key and a short-lived server-side cache of that exact response, not with a general grace window. You store token hashes, so you cannot regenerate the successor token to hand out again, and a window that reissues to anyone presenting the old token hands the thief the same door. Remember too that revocation never reaches an access token already in the wild; it dies on its own schedule, which is why that schedule stays short.

Ask AI for it

Implement refresh token rotation with reuse detection. Store only hashes of refresh tokens, with columns for family_id, parent_id, used_at, expires_at, an absolute family deadline, device label, and last-seen IP. On POST /auth/refresh: reject unknown or expired tokens with 401; rotate atomically in one conditional update so two simultaneous redemptions cannot both succeed; and treat any use of an already-used token as compromise, revoking every token sharing its family_id, logging a security event, and forcing re-login. On the client, serialize refreshes through a single shared in-flight promise so racing tabs never present the same token twice. If retry replay is genuinely required, require an idempotency key and keep the exact encrypted response in a short-lived server-side cache keyed by it. Do not build a general grace window in which any holder of the old token receives the successor. Sender-constrain the refresh token with DPoP or mTLS where the stack supports it. Add revokeSession(id) and revokeAllSessions(userId) helpers, call the second one on password change and MFA reset, and expose a settings screen listing active sessions with a sign-out button per device.

You might have meant

refresh tokenaccess token vs refresh tokenglobal sign outsessionaudit log

Go deeper