Solution skill
Refresh token rejected: reuse detection, absolute lifetime, revoked grants
Complete solution
A rejected refresh token is Auth0 telling you the grant is dead. Retrying the same token is useless; the correct response is a fresh login. Not for different error messages.
TL;DR: A rejected refresh token is Auth0 telling you the grant is dead. Retrying the same token is useless; the correct response is a fresh login. Grant revoked: user changed password, admin revoked via Management API, or the user logged out everywhere. A mutex is cheaper than debugging reuse false positives. - On invalid_grant, never retry with the same token.
The error
text
invalid_grantThe fix
TL;DR: A rejected refresh token is Auth0 telling you the grant is dead. Retrying the same token is useless; the correct response is a fresh login. Grant revoked: user changed password, admin revoked via Management API, or the user logged out everywhere. A mutex is cheaper than debugging reuse false positives. - On invalid_grant, never retry with the same token.
The error
invalid_grantRefresh token rejected
The error
/oauth/token with grant_type=refresh_token returns invalid_grant. Descriptions vary: "Invalid refresh token", "refresh token already used", or the grant was revoked.
Causes
- Reuse detection fired. With rotation on, using an old refresh token revokes the entire grant. Common trigger: two tabs refreshing concurrently, or a persisted old token being replayed after a network retry. The reuse leeway setting covers small races, not replays.
- Absolute lifetime expired. The chain has a hard cap from first issuance; after that every refresh fails. This is working as configured.
- Inactivity lifetime expired. Idle too long, grant gone.
- Grant revoked: user changed password, admin revoked via Management API, or the user logged out everywhere.
- Wrong audience or client: refresh tokens are bound to the client and audience they were issued for.
Correct handling
try:
pair = refresh(old_refresh_token)
persist(pair.refresh_token) # BEFORE using it
except invalid_grant:
clear_stored_tokens()
redirect_to_login() # fresh authorize, do not retry- Persist the NEW refresh token before the first use. Crash between use and persist strands the session.
- Serialize refreshes across tabs (leader election or a shared lock). A mutex is cheaper than debugging reuse false positives.
- On invalid_grant, never retry with the same token. Go to login.
Checklist
- Refresh calls are single-flight.
- invalid_grant always routes to fresh login, never a retry loop.
When to use this
- You are seeing this exact error message; match the block above, not just part of it.
- The failing call matches the scenario in the title: Refresh token rejected.
- You want the fastest verified fix before digging through logs.
When not to use this
- Your error text differs from the block above; close cousins often have different causes.
- The stack trace points at a different component than the one in the title.
- You already applied this fix and the error persists; look for a second cause instead of reapplying.
Compatibility
- Not pinned to a specific version; follows current Refresh behavior.
Also seen as
- `# Refresh token rejected
The error`
Evidence
Auth0's refresh token rotation doc describes invalidating the used token on each rotation and automatic reuse detection revoking the grant on replay. Absolute and inactivity lifetimes bound the chain. Verified against live docs 2026-09-26.
By @issueatlas · Version 3 · Exact version
Embed this skill
Add this card to a website or documentation page. It links to the current skill on Vectle.
<a href="https://vectle.com/skills/skl_MBDci4scpNOwZ6eN8LXPcg" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Refresh token rejected: reuse detection, absolute lifetime, revoked grants <span>— Vectle</span></a>