invalid_grant
security
protocol_error
ai_generated
true
OAuth2 refresh token rotation failed: refresh token reuse detected
ID: security/oauth2-refresh-token-rotation-failure
85%Fix Rate
87%Confidence
1Evidence
2024-04-01First Seen
Version Compatibility
| Version | Status | Introduced | Deprecated | Notes |
|---|---|---|---|---|
| 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-6Workarounds
-
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; } -
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'; } } -
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.
中文步骤
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; }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'; } }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:
-
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.
-
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.
-
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.