Refresh tokens and rotation
In December, the printer sends demo-refresh-token-3 to the token endpoint and receives demo-access-token-9 and demo-refresh-token-4 in return. The refresh token grant lesson followed that exchange from the printer's side: send the current refresh token, save its replacement, and never run two refreshes for one connection at once.
On the photo service's side, the same exchange is a small database transaction with large consequences. The authorization server has to recognize a token it never stored in usable form, decide whether it may still work, replace it exactly once, and notice when something is wrong.
What the server keeps
When you approved the printer, the photo service recorded a grant: the standing authorization that you, user-2048, gave photo-printer for photos.read. The refresh tokens it issues hang from that record. After the December refresh, a simplified view of the server's records, with fictional values, looks like this:
Grant
Subject: user-2048
Client: photo-printer
Scope: photos.read
Approved: 2026-10-01 09:00 UTC
Status: active
Refresh token family
Grant: the grant above
Started: 2026-10-01 09:00 UTC
Current token (SHA-256): _ipGaH26D00Ly1DMySWKcx37F7ecbArpfNZxIajZX9Y
Used tokens (SHA-256): STpHwz_fPJDreVsTgq-71T-jNb--CPNg4hjn_2T_RBg
Last used: 2026-12-01 09:00 UTC
Idle deadline: 2028-01-05 09:00 UTC
Absolute deadline: 2028-10-01 09:00 UTC
Status: active
The refresh tokens themselves are not in the record. OAuth leaves token storage to each authorization server, and the photo service has chosen to keep only a SHA-256 hash of each one, shown here in Base64url: the current entry is the hash of demo-refresh-token-4, and the used entry is the hash of demo-refresh-token-3. When a token arrives, the server hashes it and looks for that hash, so a stolen copy of the database contains nothing that can be presented at the token endpoint. Passwords need slow, salted hashing because people choose guessable ones. A refresh token is long and random, so a single fast hash is enough.
The token family is the chain of refresh tokens descended from one issuance, each replacing the one before. Keeping the hashes of used tokens lets the server recognize one if it ever comes back. The client binding matters as much as the hash: a refresh token issued to photo-printer is refused when any other client presents it.
Lifetimes with limits
A refresh token that never expired would carry your approval forward long after you had forgotten giving it. The two deadlines in the record bound it in different ways.
The idle lifetime ends a family that has gone unused for too long. Current security guidance says refresh tokens should expire after a period without use, since a connection nobody uses is also one nobody is watching. Each successful refresh moves the deadline forward, so this is a sliding lifetime: the December refresh pushed it to 400 days later.
A sliding lifetime on its own never ends for a client that keeps refreshing, including a thief who holds the newest token while the legitimate client sits idle. The absolute lifetime caps the family whatever its activity. This one ends on 1 October 2028, two years after you approved, and no refresh token in the family outlives it. Then the printer has to ask you to connect again, which is also your moment to decide whether it should still have access.
The numbers are policy, and they can differ by client and by scope. The calendar refreshes once a year, so its idle lifetime must be longer than a year, or the connection would lapse every time. A refresh token that can obtain photos.delete might deserve days rather than months.
Rotating in one step
When the December request arrives, the server works through it in order:
- Authenticate the client, here
photo-printerwith its client credential. - Hash the presented token and find it.
- Check that it was issued to this client, that its family and grant are still active, and that neither deadline has passed.
- If the hash matches a used token rather than the current one, stop: this is reuse, which ends the family.
- If the request includes a
scope, check that it falls within the grant, and refuse anything broader withinvalid_scope. The new access token gets the narrower scope, but the replacement refresh token keeps the grant's full scope, so a narrow request today does not shrink what the printer can ask for next time. - Replace the refresh token and issue the access token as a single step.
The last step is where implementations go wrong. If the server checks the current token and writes its replacement as separate operations, two requests arriving together can both pass the check. Both receive new refresh tokens, and the family splits into two branches that reuse detection cannot make sense of. The replacement has to be conditional on the token still being current, inside the same transaction that records the new one:
UPDATE refresh_token_families
SET current_hash = :new_hash,
last_used_at = :now
WHERE family_id = :family_id
AND current_hash = :presented_hash
AND status = 'active';
If the statement changes one row, this request won. In the same transaction, the server adds the old hash to the family's used tokens, and only after the transaction commits does it send the new tokens. If the statement changes no rows, another request replaced the token first, and this one is handled as presenting a used token.
A lost response is the hard case. The server committed the rotation and sent demo-refresh-token-4, but the connection dropped and the printer never saw it. The printer still holds demo-refresh-token-3, which is now a used token. If it tries again, strict reuse detection ends the family, and you have to reconnect the calendar because of a network fault.
Some servers soften this by accepting the token just replaced for a short grace period, a matter of seconds, instead of treating it as reuse at once. No standard defines such a period, so it is a design choice, and its cost is real: a thief who presents a stolen token inside that window is not detected either. Servers that make this choice usually keep the period short, apply it only to the immediately previous token presented by the same client, and still treat anything older as reuse.
Ending a family
When a used token comes back outside any grace period, the server cannot tell whether the printer made a mistake or someone else holds a copy, so it revokes the family. The current token stops working too, and the server records a security event against the grant. Where it can, it also invalidates the access tokens the family produced, although JWT access tokens already issued stay usable until they expire at APIs that validate them locally.
Reuse is not the only reason a family should end. These events should end it too, and some should end the grant itself:
| Event | What ends |
|---|---|
| You remove the printer from your connected applications at the photo service | The grant and every family under it. Any new connection needs your approval again. |
| Your password is reset, your account is recovered, or you sign out everywhere | Families for your grants, as the service's policy decides. Current guidance allows revocation on security events like these. |
The photo service suspends photo-printer | Every family issued to that client. |
| The printer's client credential leaks | Depending on what the logs show, every family issued to that client, since a stolen refresh token plus the credential would keep working. |
| A used refresh token is presented again | That family. |
Each of these reaches the printer the same way: its next refresh fails with invalid_grant, which the refresh token grant lesson explained means asking you to reconnect. The server owes the printer no further explanation, but it does owe you one: a list of connected applications that shows when each was last used, and a notice when one was cut off because of suspected misuse.
Refresh token theft, in Security and failure cases, follows a thief through the race between a stolen refresh token and the legitimate one, and shows where these limits stop it.