invalid_grant security protocol_error ai_generated true

OAuth2 refresh token rotation failed: refresh token reuse detected

ID: security/oauth2-refresh-token-rotation-failure

Also available as: JSON · Markdown · 中文
85%Fix Rate
87%Confidence
1Evidence
2024-04-01First Seen

Version Compatibility

VersionStatusIntroducedDeprecatedNotes
OAuth 2.0 RFC 6749 active
Auth0 OIDC 2.0 active
Keycloak 21.0.0 active
Microsoft Identity Platform v2.0 active

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.

generic

中文

OAuth2授权服务器检测到之前使用过的刷新令牌被重用,表明可能存在令牌窃取;服务器撤销了发给该客户端的所有令牌。

Official Documentation

https://datatracker.ietf.org/doc/html/rfc6749#section-6

Workarounds

  1. 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; }
    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. 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'; } }
    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. 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.
    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.

中文步骤

  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;
    }
  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';
      }
    }
  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.

Dead Ends

Common approaches that don't work:

  1. Ignore the error and retry the same refresh token 100% fail

    Once the server detects reuse, it revokes all tokens; retrying the same token will always fail.

  2. Use a new refresh token without going through the full authorization flow 90% fail

    The server expects the client to obtain a new refresh token via a fresh authorization code or client credentials flow; there is no shortcut.

  3. Store multiple refresh tokens and rotate among them 95% fail

    The server tracks reuse per token; any reused token will trigger revocation, regardless of rotation strategy.