{
  "id": "security/oauth2-refresh-token-rotation-failure",
  "signature": "OAuth2 refresh token rotation failed: refresh token reuse detected",
  "signature_zh": "OAuth2刷新令牌轮换失败：检测到刷新令牌重用",
  "regex": "invalid_grant.*refresh token.*reuse|refresh token.*already used|token.*rotation.*failed",
  "domain": "security",
  "category": "protocol_error",
  "subcategory": null,
  "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.",
  "root_cause_type": "generic",
  "root_cause_zh": "OAuth2授权服务器检测到之前使用过的刷新令牌被重用，表明可能存在令牌窃取；服务器撤销了发给该客户端的所有令牌。",
  "versions": [
    {
      "version": "OAuth 2.0 RFC 6749",
      "introduced": null,
      "deprecated": null,
      "removed": null,
      "behavior_change": null,
      "status": "active"
    },
    {
      "version": "Auth0 OIDC 2.0",
      "introduced": null,
      "deprecated": null,
      "removed": null,
      "behavior_change": null,
      "status": "active"
    },
    {
      "version": "Keycloak 21.0.0",
      "introduced": null,
      "deprecated": null,
      "removed": null,
      "behavior_change": null,
      "status": "active"
    },
    {
      "version": "Microsoft Identity Platform v2.0",
      "introduced": null,
      "deprecated": null,
      "removed": null,
      "behavior_change": null,
      "status": "active"
    }
  ],
  "os_specific": {},
  "dead_ends": [
    {
      "action": "Ignore the error and retry the same refresh token",
      "why_fails": "Once the server detects reuse, it revokes all tokens; retrying the same token will always fail.",
      "fail_rate": 1.0,
      "condition": "",
      "sources": []
    },
    {
      "action": "Use a new refresh token without going through the full authorization flow",
      "why_fails": "The server expects the client to obtain a new refresh token via a fresh authorization code or client credentials flow; there is no shortcut.",
      "fail_rate": 0.9,
      "condition": "",
      "sources": []
    },
    {
      "action": "Store multiple refresh tokens and rotate among them",
      "why_fails": "The server tracks reuse per token; any reused token will trigger revocation, regardless of rotation strategy.",
      "fail_rate": 0.95,
      "condition": "",
      "sources": []
    }
  ],
  "workarounds": [
    {
      "action": "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:\n\nasync function refreshAccessToken(refreshToken) {\n  const response = await fetch('https://auth.example.com/token', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n    body: new URLSearchParams({\n      grant_type: 'refresh_token',\n      refresh_token: refreshToken,\n      client_id: 'your-client-id',\n      client_secret: 'your-client-secret'\n    })\n  });\n  const data = await response.json();\n  if (data.refresh_token) {\n    // Securely store the new refresh token\n    await storeRefreshToken(data.refresh_token);\n  }\n  return data.access_token;\n}",
      "success_rate": 0.9,
      "how": "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:\n\nasync function refreshAccessToken(refreshToken) {\n  const response = await fetch('https://auth.example.com/token', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n    body: new URLSearchParams({\n      grant_type: 'refresh_token',\n      refresh_token: refreshToken,\n      client_id: 'your-client-id',\n      client_secret: 'your-client-secret'\n    })\n  });\n  const data = await response.json();\n  if (data.refresh_token) {\n    // Securely store the new refresh token\n    await storeRefreshToken(data.refresh_token);\n  }\n  return data.access_token;\n}",
      "condition": "",
      "sources": []
    },
    {
      "action": "Use short-lived refresh tokens and implement a fallback to the authorization code flow if refresh fails. For example, in Auth0:\n\nconst client = new OAuth2Client({\n  clientId: 'your-client-id',\n  authorizationUri: 'https://auth.example.com/authorize',\n  tokenUri: 'https://auth.example.com/token',\n  redirectUri: 'https://app.example.com/callback',\n  usePKCE: true,\n  refreshTokenRotation: true\n});\ntry {\n  const token = await client.refreshToken(oldRefreshToken);\n} catch (error) {\n  if (error.message.includes('refresh token reuse')) {\n    // Redirect user to login again\n    window.location.href = '/login';\n  }\n}",
      "success_rate": 0.85,
      "how": "Use short-lived refresh tokens and implement a fallback to the authorization code flow if refresh fails. For example, in Auth0:\n\nconst client = new OAuth2Client({\n  clientId: 'your-client-id',\n  authorizationUri: 'https://auth.example.com/authorize',\n  tokenUri: 'https://auth.example.com/token',\n  redirectUri: 'https://app.example.com/callback',\n  usePKCE: true,\n  refreshTokenRotation: true\n});\ntry {\n  const token = await client.refreshToken(oldRefreshToken);\n} catch (error) {\n  if (error.message.includes('refresh token reuse')) {\n    // Redirect user to login again\n    window.location.href = '/login';\n  }\n}",
      "condition": "",
      "sources": []
    },
    {
      "action": "If using Keycloak, configure refresh token revocation on reuse in the realm settings:\n\nKeycloak Admin Console -> Realm Settings -> Tokens -> 'Revoke Refresh Token' = ON\n\nThis ensures that any reuse triggers immediate revocation.",
      "success_rate": 0.8,
      "how": "If using Keycloak, configure refresh token revocation on reuse in the realm settings:\n\nKeycloak Admin Console -> Realm Settings -> Tokens -> 'Revoke Refresh Token' = ON\n\nThis ensures that any reuse triggers immediate revocation.",
      "condition": "",
      "sources": []
    }
  ],
  "workarounds_zh": [
    "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:\n\nasync function refreshAccessToken(refreshToken) {\n  const response = await fetch('https://auth.example.com/token', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n    body: new URLSearchParams({\n      grant_type: 'refresh_token',\n      refresh_token: refreshToken,\n      client_id: 'your-client-id',\n      client_secret: 'your-client-secret'\n    })\n  });\n  const data = await response.json();\n  if (data.refresh_token) {\n    // Securely store the new refresh token\n    await storeRefreshToken(data.refresh_token);\n  }\n  return data.access_token;\n}",
    "Use short-lived refresh tokens and implement a fallback to the authorization code flow if refresh fails. For example, in Auth0:\n\nconst client = new OAuth2Client({\n  clientId: 'your-client-id',\n  authorizationUri: 'https://auth.example.com/authorize',\n  tokenUri: 'https://auth.example.com/token',\n  redirectUri: 'https://app.example.com/callback',\n  usePKCE: true,\n  refreshTokenRotation: true\n});\ntry {\n  const token = await client.refreshToken(oldRefreshToken);\n} catch (error) {\n  if (error.message.includes('refresh token reuse')) {\n    // Redirect user to login again\n    window.location.href = '/login';\n  }\n}",
    "If using Keycloak, configure refresh token revocation on reuse in the realm settings:\n\nKeycloak Admin Console -> Realm Settings -> Tokens -> 'Revoke Refresh Token' = ON\n\nThis ensures that any reuse triggers immediate revocation."
  ],
  "transition_graph": {
    "leads_to": [],
    "preceded_by": [],
    "frequently_confused_with": []
  },
  "official_doc_url": "https://datatracker.ietf.org/doc/html/rfc6749#section-6",
  "official_doc_section": null,
  "error_code": "invalid_grant",
  "verification_tier": "ai_generated",
  "confidence": 0.87,
  "fix_success_rate": 0.85,
  "resolvable": "true",
  "first_seen": "2024-04-01",
  "last_confirmed": "2024-06-01",
  "last_updated": "2024-06-01",
  "evidence_count": 1,
  "tags": [],
  "locale": "en",
  "aliases": []
}