# Refresh 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

1. 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.
2. Absolute lifetime expired. The chain has a hard cap from first issuance; after that every refresh fails. This is working as configured.
3. Inactivity lifetime expired. Idle too long, grant gone.
4. Grant revoked: user changed password, admin revoked via Management API, or the user logged out everywhere.
5. 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.