TL;DR
refresh_token_reused= you (or a concurrent process) sent a refresh token that was already rotated and revoked.- Providers (Okta, Auth0, Salesforce) treat this as possible token theft → revoke the whole token family → user logged out everywhere.
- Most innocent cause: concurrent refresh — two callers POST the same token, first wins, second is flagged as "reuse."
- Fix: single-flight in-process + cross-process lock if credential is shared across processes. Never send the same refresh token twice.
How reuse detection works
Providers that implement refresh-token rotation (Okta, Auth0, Salesforce, and others) maintain a chain of refresh tokens per authorization grant. When a refresh succeeds:
- The provider marks
R0as "rotated" (used and revoked). - It issues
R1— the new refresh token for this grant. - If it later receives another request using
R0, it knowsR0was already rotated. This is ambiguous: it could be a concurrent innocent caller, or it could be an attacker who stoleR0before it was rotated and is now replaying it. - Because the provider cannot distinguish the two cases, it takes the safe path: revoke the entire token family — all refresh tokens derived from the original authorization, and typically the active access tokens too. The user is logged out everywhere.
The most common innocent trigger: concurrency
The legitimate-looking sequence that trips reuse detection:
t0 access token expires (or hits the skew window)
t1 caller A POSTs refresh_token=R0
t2 caller B POSTs refresh_token=R0 // concurrent — same token
t3 provider processes A → issues R1, marks R0 rotated
t4 provider processes B → R0 is rotated → refresh_token_reused
→ ENTIRE token family revoked
→ user logged out everywhere
Caller B is innocent — it read the same credential that caller A read, found it expired, and refreshed. But the provider cannot distinguish this from an attacker replaying a stolen R0.
Other innocent triggers
- Retry without re-reading: your code catches
invalid_grant, retries the same refresh token. If another process or a previous call already rotated it, the retry sends a revoked token and trips reuse detection. - Stale credential in cache: an in-memory copy of creds doesn't see the updated token written by another process. It refreshes with an already-rotated token.
- Deploy rollover: two versions of your service run briefly in parallel. One version refreshes a token; the other retains the old token in memory and uses it shortly after.
- Token file not read after locking: a process acquires a lock, but doesn't re-read the token file from disk — it refreshes with a token that a previous lock holder already rotated.
The fix
Serialize refresh so that only one refresh request ever fires per expiry event per credential:
- In-process single-flight — one shared in-flight
Promise; all concurrent callers await it. Covers the common case: one server, one worker. See code. - Cross-process lock — if multiple OS processes share one credential file: exclusive lock (
O_CREAT|O_EXCL) around refresh; re-read the token after acquiring the lock (the previous holder may have already rotated); write atomically (temp file +rename). See code. - On
invalid_grant: re-read before failing — before surfacing the error to the user, re-read the stored credential once; a sibling may have already refreshed and written a fresh token. If the fresh token is valid, use it and recover silently.
Provider-specific notes
| Provider | Error code | Whole family revoked? | Reuse detection configurable? |
|---|---|---|---|
| Okta | invalid_grant or refresh_token_reused | Yes (by default) | Yes, per-app — rotation can be disabled, but that weakens security |
| Auth0 | invalid_grant + error body may mention reuse | Yes (by default with rotation) | Rotation configurable per-application in Auth0 dashboard |
| Salesforce | invalid_grant | Yes | Not typically configurable; rotation is enforced |
invalid_grant | No (does not rotate) | No rotation → no reuse detection in this sense | |
| Microsoft | invalid_grant | Depends on tenant config | Rotation on by default for public clients |
Verify against each provider's current documentation — rotation and reuse-detection behavior can be configured at the tenant or application level.
Library
refresh-guard implements single-flight and correct rotation-merge, eliminating the most common cause of refresh_token_reused (in-process concurrency) as a built-in default.
npm i refresh-guardimport { createTokenManager, fileStore } from "refresh-guard";
const tokens = createTokenManager({
provider: "okta", // loads the Okta quirks profile: rotation + reuse-detection aware
store: fileStore("~/.app/creds.json"),
refresh: async (prev) => fetchNewToken(prev.refresh_token)
});
const access = await tokens.getValidToken();
Source on GitHub · npm i refresh-guard. The in-process race is the most common cause; for multi-process scenarios, pair with a cross-process lock.
FAQ
What does refresh_token_reused mean?
The provider received a refresh token that was already rotated and revoked. Providers with reuse detection (Okta, Auth0, Salesforce) treat this as a possible token-theft replay and revoke the entire token family, logging the user out everywhere.
What causes refresh_token_reused in practice?
Usually a concurrency bug: two callers each fire a refresh with the same token. The first succeeds and rotates; the second sends the now-revoked token and triggers reuse detection. Other causes: retry logic that reuses the same token, stale in-memory creds, or deploy rollovers with two versions running briefly in parallel.
Why does reuse detection revoke the whole token family?
The provider cannot distinguish a concurrent innocent caller from an attacker replaying a stolen token. The safe action is to revoke both — the entire chain of tokens from the original authorization. This is disruptive (full session logout) but prevents a legitimate-appearing attacker from continuing to use a stolen token.
How do I prevent refresh_token_reused?
Serialize refresh: single-flight in-process (one shared Promise, all callers await it) and a cross-process lock if multiple OS processes share one credential file. Re-read the stored token after acquiring the lock and short-circuit if another process already rotated. Never send the same refresh token twice.
Can I disable reuse detection to avoid the problem?
Technically yes (per-app in Okta/Auth0), but it weakens your security posture by making stolen refresh tokens harder to detect. The correct fix is to eliminate the concurrency, not remove the detection mechanism.