OAuth2 engineering › Token lifecycle › refresh_token_reused & reuse detection

refresh_token_reusedwhat it means and how to fix it

Reuse detection on Okta, Auth0, and Salesforce treats a concurrent-refresh race the same as a token-theft replay. It revokes the whole token family. Here's why it happens and how to prevent it.

Applies to Okta, Auth0, Salesforce · no trackers, no cookies. Last updated 2026-05-31.

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:

  1. The provider marks R0 as "rotated" (used and revoked).
  2. It issues R1 — the new refresh token for this grant.
  3. If it later receives another request using R0, it knows R0 was already rotated. This is ambiguous: it could be a concurrent innocent caller, or it could be an attacker who stole R0 before it was rotated and is now replaying it.
  4. 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.
A single concurrent refresh can log out your entire user session, not just fail one request. This makes serializing refresh a correctness requirement, not just an optimization.

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:

  1. In-process single-flight — one shared in-flight Promise; all concurrent callers await it. Covers the common case: one server, one worker. See code.
  2. 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.
  3. 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.
Do not disable rotation to fix the race. Disabling rotation removes the single-use property that makes token theft detectable. The correct fix is to eliminate the concurrency, not to weaken the security contract.

Provider-specific notes

ProviderError codeWhole family revoked?Reuse detection configurable?
Oktainvalid_grant or refresh_token_reusedYes (by default)Yes, per-app — rotation can be disabled, but that weakens security
Auth0invalid_grant + error body may mention reuseYes (by default with rotation)Rotation configurable per-application in Auth0 dashboard
Salesforceinvalid_grantYesNot typically configurable; rotation is enforced
Googleinvalid_grantNo (does not rotate)No rotation → no reuse detection in this sense
Microsoftinvalid_grantDepends on tenant configRotation 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-guard
import { 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.