Hi again @Arthur_Dent
I am sorry for the delayed response as I have been out of office recently and could not get back to you with a prompt reply.
Your mobile apps (iOS and Android) are returning "Unknown or invalid refresh token" errors for some users, even though the refresh tokens should not be expired. You are using the Auth0 native SDKs with proper configuration, including the offline_access scope and appropriate refresh token settings. The error occurs intermittently for some users, suggesting a race condition or token rotation issue rather than a configuration problem.
The issue is most likely caused by a race condition in the credentials manager where concurrent token refresh requests cause both the original and rotated refresh tokens to be invalidated simultaneously. This can happen when Refresh Token Rotation is enabled and multiple refresh attempts occur in quick succession, especially over unstable network connections.
[Root Cause]
Auth0's Refresh Token Rotation feature automatically invalidates the old token when a new one is issued. However, in mobile apps, several factors can trigger race conditions:
- Concurrent refresh requests — If your app calls
credentialsManager.credentials() or credentialsManager.getCredentials() multiple times in quick succession (e.g., from different screens or background tasks), both requests may attempt to use the same refresh token before either receives the rotated token.
- Rotation Overlap Period too short — If the overlap period is set too low (or to 0), the old token is invalidated before the new token is fully propagated to the credentials manager. This creates a window where both tokens are invalid.
- Token reuse detection — Auth0 detects when the same refresh token is used twice and invalidates the entire token family as a security measure to prevent replay attacks.
- Unstable network conditions — The known SDK bug affects token rotation behavior when network requests are interrupted or retried, causing the credentials manager to attempt reuse of the same token.
- Absolute expiration reached — Even with rotation enabled, refresh tokens have an absolute expiration time. If a user's session is inactive and the absolute expiration is reached, the token becomes invalid.
When Auth0 rejects a refresh token, the logs show a fertft (Failed Exchange of Refresh Token) event. The token is rejected for one of three reasons:
- The token was intentionally revoked
- The user hit the absolute device limit
- The token family was purged due to a perceived replay attack (reuse detection)
[Solution]
Step 1: Verify Refresh Token Rotation settings
Navigate to Auth0 Dashboard → Applications → Your Application → Settings → Advanced → Refresh Token Rotation.
Check the following:
- Rotation enabled: Confirm this is enabled (it should be for security)
- Rotation Overlap Period: Increase from the default (e.g., from 0 seconds to 10–30 seconds). This gives the credentials manager time to update before the old token is invalidated
- Absolute Expiration: Verify the absolute expiration time is appropriate for your use case (e.g., 7 days or longer)
- Idle Expiration: Ensure the idle expiration is not too short (e.g., set to 30 days or longer)
Step 2: Test by disabling Refresh Token Rotation (diagnostic only)
To isolate whether rotation is the cause:
- Navigate to Auth0 Dashboard → Applications → Your Application → Settings → Advanced → Refresh Token Rotation
- Toggle off "Allow Refresh Token Rotation"
- Test your app with a few users to see if the error disappears
If the error stops after disabling rotation, the issue is confirmed to be rotation-related. In this case, re-enable rotation and proceed to Step 3.
Step 3: Increase the Rotation Overlap Period
If disabling rotation resolves the issue, re-enable it and increase the overlap period:
- Navigate to Auth0 Dashboard → Applications → Your Application → Settings → Advanced → Refresh Token Rotation
- Enable "Allow Refresh Token Rotation"
- Set Rotation Overlap Period to 10–30 seconds (instead of 0)
- Click Save
This gives the credentials manager a larger window to update before the old token is invalidated, reducing race condition likelihood.
Step 4: Analyze Auth0 logs for token rejection reasons
To understand why specific users are experiencing the error, you must examine the Auth0 logs in a specific sequence:
- Navigate to Auth0 Dashboard → Logs
- Search for the user experiencing the error
- Track the token lifecycle by looking for these event types in order:
rrrt — Refresh Token Rotation event (when a new token is issued)
resource_cleanup — Token cleanup event (when old tokens are purged)
fertft — Failed Exchange of Refresh Token event (the error)
- Read the
fertft event description — It will indicate one of three reasons:
- "Token could not be decoded or is missing in DB" → Token was revoked or family was purged
- "Device limit exceeded" → User hit the absolute device limit
- "Reuse detection triggered" → Token family was purged due to replay attack detection
- Look at logs immediately preceding the
fertft event — The true root cause (e.g., why the token was revoked or why reuse was detected) is almost always found in the logs immediately before the error, not in the error itself.
Step 5: Ensure credentials manager is used correctly
Verify that your app is not making concurrent calls to the credentials manager:
iOS:
// CORRECT: Single call to credentials manager
credentialsManager.credentials(minTTL: 6 * 60) { result in
switch result {
case .success(let credentials):
// Use credentials
case .failure(let error):
// Handle error
}
}
// INCORRECT: Multiple concurrent calls
DispatchQueue.global().async {
credentialsManager.credentials(minTTL: 6 * 60) { _ in }
}
DispatchQueue.global().async {
credentialsManager.credentials(minTTL: 6 * 60) { _ in }
}
Android:
// CORRECT: Single call to credentials manager
credentialsManager.getCredentials(
null,
6 * 60,
object : Callback<Credentials, CredentialsManagerException> {
override fun onSuccess(credentials: Credentials) {
// Use credentials
}
override fun onFailure(error: CredentialsManagerException) {
// Handle error
}
}
)
// INCORRECT: Multiple concurrent calls
Thread {
credentialsManager.getCredentials(null, 6 * 60, callback)
}.start()
Thread {
credentialsManager.getCredentials(null, 6 * 60, callback)
}.start()
Step 6: Verify useEphemeralSession() is not the cause (iOS)
The useEphemeralSession() parameter does not cause refresh token invalidation. This is a common misconception:
useEphemeralSession() prevents the login cookie from being saved to the shared Safari cookie jar (acts like an incognito tab)
- Refresh tokens are stored securely in the iOS Keychain and are retrieved via direct HTTP requests to Auth0's
/oauth/token endpoint
- Browser cookies are not involved in refresh token exchanges — they happen entirely through backend API calls
- Therefore, an ephemeral session cannot physically break a refresh token
Your use of useEphemeralSession() is correct and is not the cause of the error.
Step 7: Update to the latest SDK version
Auth0 has released fixes for token rotation race conditions in the native SDKs, particularly for unstable network scenarios:
- For iOS: Update to the latest version of
Auth0.swift
- For Android: Update to the latest version of
auth0-android
Check your current versions:
iOS (CocoaPods):
pod 'Auth0', '~> 2.x' # Use latest 2.x version
Android (Gradle):
implementation 'com.auth0.android:auth0:2.x.x' # Use latest version
Step 8: If errors persist, escalate to Auth0 Support
If the error continues after applying these steps, open an issue on the Auth0 GitHub repository with:
- SDK version (iOS and Android)
- Refresh token configuration (rotation settings, expiration times, overlap period)
- Auth0 log IDs showing the
fertft events and the logs immediately preceding them
- Steps to reproduce (if possible)
- Network conditions when the error occurs (WiFi, cellular, unstable connection)
This is a known pain point for native applications, and the Auth0 SDK team actively monitors GitHub issues for token rotation problems.
Why This Happens: Technical Details
When Refresh Token Rotation is enabled:
- User calls
credentialsManager.credentials() to get a token
- Auth0 checks if the access token is expired or near expiration
- If expired, Auth0 exchanges the refresh token for a new access token AND a new refresh token
- The old refresh token is immediately invalidated (or after the overlap period expires)
- Auth0 returns the new tokens to the credentials manager
The race condition occurs when:
- Request A calls
credentialsManager.credentials() at time T
- Request B calls
credentialsManager.credentials() at time T+100ms (before Request A completes)
- Both requests use the same refresh token (the old one)
- Auth0 detects the reuse and invalidates the entire token family
- Both requests fail with "Unknown or invalid refresh token"
The Rotation Overlap Period prevents this by:
Follow these diagnostic steps in order. Start with Step 1 to verify your configuration, then Step 2 to isolate the root cause. The key to resolving this issue is analyzing the Auth0 logs in the correct sequence (Step 4) to identify whether the problem is rotation-related, network-related, or configuration-related.
Kind Regards,
Nik