# Refresh token rotation under retries, multiple tabs, and packet loss

> Distinguish token theft from ordinary refresh races with token families, short grace rules, client coordination, and observable recovery.

Canonical URL: https://www.devobs.io/articles/refresh-token-rotation-race-conditions/
By: Ines Costa
Published: 2025-06-12T21:16:48.684Z
Updated: 2026-09-05
Section: Identity

Refresh-token rotation should treat each successful refresh as a state transition in a token family, while giving normal concurrency a narrowly bounded recovery path. The server must still detect reuse, but two browser tabs and a lost response should not be indistinguishable from a stolen token by accident.

## Model the family, not isolated strings

Store an immutable family identifier, a generation or parent link, issuance time, status, and client/session binding for every refresh token. On a successful exchange, mark the presented token consumed and issue its child atomically. A token presented after consumption is reuse. The important question is what to do next.

The OAuth 2.0 Security Best Current Practice, [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700), describes refresh-token rotation as issuing a new refresh token with every access-token refresh, retaining relationship information, and revoking the active token when reuse reveals a breach. Sender-constrained refresh tokens are the other replay-detection approach it identifies for public clients.

A browser creates benign races. Tab A and tab B can submit the same current token. A mobile client can retry after the server commits rotation but the response disappears. Without coordination, the second request looks exactly like replay. Start by serializing refreshes in the client: one in-flight promise per session, shared across callers. In browsers, coordinate tabs with a shared worker, service worker, or a short-lived lock backed by browser storage, while assuming that coordination can fail.

## Keep any grace rule narrow

A server grace rule can return the already-created child to an immediate duplicate with the same client binding, rather than rotating again. Bound it by a few properties, not only time: same parent token, same client and session, same requested scope or audience, and no later generation already used. Store the original result securely enough to replay it. Never create siblings from the same parent; branching makes later reuse decisions ambiguous.

Grace weakens the signal because an attacker inside the window can resemble a retry. Use the smallest window supported by observed retry latency, and record whether grace was exercised. If a consumed token appears after the window, from a different binding, or after its child has been used, revoke the active family and require authentication. Alert on repeated events rather than turning every single network retry into an incident.

Ory documents the OAuth 2.0 [refresh token grant](https://www.ory.com/docs/oauth2-oidc/refresh-token-grant) and the request to the token endpoint. Ory Hydra provides OAuth 2.0 and OpenID Connect and can integrate with an existing user-management system. Deployment-specific rotation behavior should be verified against the current configuration and documentation; the concurrency policy described here belongs in the complete client and authorization-service design, whether using Ory Network or a self-hosted project.

## Make recovery an explicit product state

When the family is revoked, return a stable machine-readable error and clear local credentials before redirecting to authentication. Avoid infinite refresh loops. Preserve enough audit context to explain family identifier, generation, client, reuse timing, and response, without logging raw tokens. Metrics should separate successful rotations, grace replays, rejected reuse, and forced reauthentication.

Test four timelines: simultaneous tabs, lost refresh response followed by retry, stolen parent used after the legitimate child, and delayed offline retry after several generations. Define the expected family state after every step. The next engineering task is to draw those state transitions and implement them as transactional tests before enabling rotation for real sessions.

## Keep atomicity at the token endpoint

The consume-and-issue step must be one transaction or one compare-and-swap operation. Test two workers reading the same active parent before either writes. Exactly one may create the child; the other must follow the documented duplicate or reuse path. A database uniqueness constraint on the parent generation is a useful final guard against application races.

## Source references

- <https://www.rfc-editor.org/rfc/rfc9700>
- <https://www.ory.com/docs/oauth2-oidc/refresh-token-grant>
