Refresh-token rotation and reuse detection
“What is refresh-token rotation, and how does reuse detection catch a stolen token?”
Every refresh token works once. If a used one comes back, someone copied it, so the whole family is cut.
See it
R1: unused · F
1/7After sign-in, the portal holds refresh token R1. The store keeps R1's hash, in family F.
In plain words
Access tokens are short: about 15 minutes. When one expires, the app sends its refresh token to the API's refresh endpoint and gets a new access token, so the user doesn't sign in again. A refresh token lives much longer (days), so stealing one is worth a lot. It is also opaque: a random string that means nothing by itself, only a key to a database row. That is why it can be cancelled and an access token can't: the API never looks an access token up, it only checks its signature (), so nothing on the server can cancel one before it expires. Below, R1 and R2 are just names for the first and second refresh token. Two rules make theft short-lived. Rotation: each refresh token works once. When the app uses it, the server marks it used and hands back a new refresh token with the new access token. Reuse detection: all the refresh tokens that grew from one sign-in form a family. If a token already marked used is sent again, two parties hold copies: the real app and a thief. The server can't tell which is which, so it revokes the whole family. Both get 401 (the HTTP status for "not signed in") and must sign in again, and the thief's copy is dead. The server stores each refresh token as a hash (a one-way fingerprint), never the token itself, so a database leak doesn't hand out live tokens.
- Refresh tokens are long-lived, opaque random strings (they mean nothing by themselves), kept server-side as hashes. The API looks them up on every use, so unlike a JWT access token, which is only signature-checked, they can be revoked.
- With rotation, every refresh returns a new refresh token and marks the old one used; a refresh token is single-use.
- Reuse detection groups tokens into a family per sign-in. If a used token is presented again, it was copied, so I revoke the entire family and force a new sign-in. Whichever side uses the old token second trips detection, so a stolen token gives the thief a session that lasts only until the real client next refreshes, plus whatever is left of the access token it already holds, up to 15 minutes. If the real client never comes back, the thief could keep rotating, so I also cap the family's total lifetime.
- The catch is concurrency: a client that sends two refreshes at once trips reuse detection by itself. The client must refresh single-flight, which means one refresh at a time while other calls wait for it. Some servers also accept the just-rotated token for a few seconds (a grace window), trading a little detection for fewer false sign-outs.
In the lending API
The core of a refresh endpoint's service, cut down to the rotation rules. Tap Next line to walk the thief's request.
Lending.sln · where this file sits
- src/Auth/
What they ask next
Interviewers check that you can explain every line. Tap a question to open it.
Two browser tabs refresh at the same moment. What happens?
The second request arrives just after the first rotated the token, so it looks like reuse and signs the user out. The fix is in two places: the client sends one refresh at a time and the other calls wait for it (single-flight), and the server may accept a just-rotated token for a short grace window. Navistar hit exactly this (composed, not a real event): . If the thief goes first, the order flips: the thief gets R2, and the portal's R1 trips detection. Same result: the family is revoked.
Why store a hash and not the token?
If the table leaks, hashes can't be sent to the refresh endpoint: it needs the real token. It is the same reason passwords are stored hashed.
Where does the browser keep the refresh token?
Best in an , cookie that JavaScript can't read, sent only to the refresh endpoint. Local storage is readable by any script on the page, so one cross-site scripting bug (a script injected into the page) leaks it. The Angular side is its own card.
Traps
- Don't say
Rotation alone stops a stolen refresh token.SayRotation makes tokens single-use; reuse detection is what notices the copy and cuts the family. - Don't say
Store refresh tokens in plain text so you can look them up.SayStore a hash and look up by the hash.