# OAuth2 refresh token rotation failed: refresh token reuse detected

- **ID:** `security/oauth2-refresh-token-rotation-failure`
- **Domain:** security
- **Category:** protocol_error
- **Error Code:** `invalid_grant`
- **Verification:** ai_generated
- **Fix Rate:** 85%

## Root Cause

The OAuth2 authorization server detected that a previously used refresh token was reused, indicating potential token theft; the server revokes all tokens issued to the client in response.

## Version Compatibility

| Version | Status | Introduced | Deprecated |
|---------|--------|------------|------------|
| OAuth 2.0 RFC 6749 | active | — | — |
| Auth0 OIDC 2.0 | active | — | — |
| Keycloak 21.0.0 | active | — | — |
| Microsoft Identity Platform v2.0 | active | — | — |

## Workarounds

1. **Implement refresh token rotation correctly: after each successful refresh, discard the old refresh token and store the new one. Example in a Node.js OAuth2 client:

async function refreshAccessToken(refreshToken) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: 'your-client-id',
      client_secret: 'your-client-secret'
    })
  });
  const data = await response.json();
  if (data.refresh_token) {
    // Securely store the new refresh token
    await storeRefreshToken(data.refresh_token);
  }
  return data.access_token;
}** (90% success)
   ```
   Implement refresh token rotation correctly: after each successful refresh, discard the old refresh token and store the new one. Example in a Node.js OAuth2 client:

async function refreshAccessToken(refreshToken) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: 'your-client-id',
      client_secret: 'your-client-secret'
    })
  });
  const data = await response.json();
  if (data.refresh_token) {
    // Securely store the new refresh token
    await storeRefreshToken(data.refresh_token);
  }
  return data.access_token;
}
   ```
2. **Use short-lived refresh tokens and implement a fallback to the authorization code flow if refresh fails. For example, in Auth0:

const client = new OAuth2Client({
  clientId: 'your-client-id',
  authorizationUri: 'https://auth.example.com/authorize',
  tokenUri: 'https://auth.example.com/token',
  redirectUri: 'https://app.example.com/callback',
  usePKCE: true,
  refreshTokenRotation: true
});
try {
  const token = await client.refreshToken(oldRefreshToken);
} catch (error) {
  if (error.message.includes('refresh token reuse')) {
    // Redirect user to login again
    window.location.href = '/login';
  }
}** (85% success)
   ```
   Use short-lived refresh tokens and implement a fallback to the authorization code flow if refresh fails. For example, in Auth0:

const client = new OAuth2Client({
  clientId: 'your-client-id',
  authorizationUri: 'https://auth.example.com/authorize',
  tokenUri: 'https://auth.example.com/token',
  redirectUri: 'https://app.example.com/callback',
  usePKCE: true,
  refreshTokenRotation: true
});
try {
  const token = await client.refreshToken(oldRefreshToken);
} catch (error) {
  if (error.message.includes('refresh token reuse')) {
    // Redirect user to login again
    window.location.href = '/login';
  }
}
   ```
3. **If using Keycloak, configure refresh token revocation on reuse in the realm settings:

Keycloak Admin Console -> Realm Settings -> Tokens -> 'Revoke Refresh Token' = ON

This ensures that any reuse triggers immediate revocation.** (80% success)
   ```
   If using Keycloak, configure refresh token revocation on reuse in the realm settings:

Keycloak Admin Console -> Realm Settings -> Tokens -> 'Revoke Refresh Token' = ON

This ensures that any reuse triggers immediate revocation.
   ```

## Dead Ends

- **Ignore the error and retry the same refresh token** — Once the server detects reuse, it revokes all tokens; retrying the same token will always fail. (100% fail)
- **Use a new refresh token without going through the full authorization flow** — The server expects the client to obtain a new refresh token via a fresh authorization code or client credentials flow; there is no shortcut. (90% fail)
- **Store multiple refresh tokens and rotate among them** — The server tracks reuse per token; any reused token will trigger revocation, regardless of rotation strategy. (95% fail)
