Jul 04, 2026 · 1 min read
Refresh token rotation, and catching token theft
Rotating refresh tokens on every use, grouping them into families, and revoking the whole family when an old one comes back.
Access tokens are short-lived, so they’re low-risk if leaked - they expire in minutes. Refresh tokens are the opposite: long-lived by design, and a stolen one is a standing key to the account. Rotation is how I keep that key from being useful for long.
Rotate on every use
Each time a refresh token is exchanged, it’s invalidated and a brand-new one is issued. A refresh token is therefore usable exactly once. In Bildora’s identity service I store a hash of the current token per session and swap it on refresh:
POST /auth/refresh (old refresh token)
-> validate + mark old token used
-> issue new access token + new refresh token
Group tokens into families
Every token descended from one login shares a family_id. Rotation moves the family forward one step at a time; the family is the unit I can revoke.
Detect theft by reuse
Here’s the payoff. If a refresh token that was already rotated out shows up again, two parties hold tokens from the same family - the legitimate user and a thief. I can’t tell which is which, so I trust neither: revoke the entire family and force a fresh login.
if token.used_at is not null: # a used token came back
revoke_family(token.family_id) # kill every session in the line
reject()
The result: a stolen refresh token works at most until the real user next refreshes - at which point the mismatch trips the alarm and both copies die. Rotation limits the window; family revocation closes it.