Unknown or invalid refresh token on Android and iOS

Greetings,

Our team is facing some problems with auth0 in mobile apps (both, Android and iOS). We have looked through a lot of topics here but couldn’t find answers to our questions.

The problem we’re facing is auth0 returns “Unknown or invalid refresh token” for some users when the refresh token is not supposed to expire.

We login our users as recommended in the documentation.

iOS
func requestWebAuthLogin(callback: @escaping (Result<Credentials, WebAuthError>) → Void) {
  Auth0
   .webAuth(client, domain) // we use our client, domain
   .scope(“openid offline_access”)
   .audience(audience) // we use our audience
   .useEphemeralSession()
   .parameters([“prompt”: “login”])
   .start { /* our callback logic  and saving credentials */ }
  }
}
Android
login(auth0) // auth0 instance with our client and domain
  .withParameters(mapOf(“prompt” to “login”))
  .withAudience(audience) // we use our audience
  .withScheme(scheme) // we use our scheme
  .withScope("openid offline_access")
  .start( /* our callback logic  and saving credentials */ )

And retrieve the token to reauthenticate them:

iOS
credentialsManager.credentials(minTTL: 6 * 60) { 
    /* our callback logic */ 
}
Android
credentialsManager.getCredentials(
    null, 
    6 * 60, 
    /* our callback logic */
)

Our refresh token configurations are:

Hi @Arthur_Dent

Welcome to the Auth0 Community!

I am sorry about the delayed response to your inquiry!

If you disable Refresh Token Rotation within the Auth0 Dashboard OR if you increase the Rotation Overlap Period, does the error go away or does it still persist?

Also, for iOS, can you try configuring your Auth0 instance with a maxRetries parameter?

const auth0 = new Auth0({
  domain: 'YOUR_DOMAIN',
  clientId: 'YOUR_CLIENT_ID',
  maxRetries: 2, // Retry up to 2 times on transient errors (iOS only)
});

It appears that the issue might be caused by a race condition where your credentialsManager is returning the 2nd refresh token after the first one has been used and they both get invalidated.

Just to confirm, the error you mentioned is the only one you receive or do you also receive an “Unsuccessful Refresh Token exchange, reused refresh token detected” error?

Kind Regards,
Nik

Greetings @nik.baleca, thank you for your response!

If you disable Refresh Token Rotation within the Auth0 Dashboard OR if you increase the Rotation Overlap Period, does the error go away or does it still persist?

We haven’t tried disabling refresh token rotation or increasing the rotation overlap period. We might try that, however it would be great to get reference in the documentation or rationale/context behind this question so that we could dig further and localise the problem.

We can provide more information around our refresh token configs.

Just to confirm, the error you mentioned is the only one you receive or do you also receive an “Unsuccessful Refresh Token exchange, reused refresh token detected” error?

We haven’t found this error in our logs.

Thanks

Any help would be appreciated.

Hi again @Arthur_Dent

I am truly sorry about the delayed reply to your last update regarding the matter! I must have missed the initial notification.

We haven’t tried disabling refresh token rotation or increasing the rotation overlap period. We might try that, however it would be great to get reference in the documentation or rationale/context behind this question so that we could dig further and localise the problem.

The error message that you are receiving would indicate the possibility of your application re-using the refresh token or that it is trying to use both refresh tokens (1st one initially issued and the one that is rotated) during a single request, either due to a race condition, absolute expiration values, reuse period settings or even custom login within your application.

You can read more about that inside this knowledge article.

Otherwise, that is why I suggested the above to see if the refresh token rotation is the cause of the behaviour that you are experiencing. I would highly recommend testing the refresh token expiration/rotation settings to make sure that they are not getting invalidated during the user’s session.

In order to provide more context about the error mentioned previously, there is a known bug/issue in regards to the Native SDKs where the team provided updates to mitigate the issue. You can read more about that here.

Can you provide some extra information on what SDKs are you using for Android and iOS respectively?

Kind Regards,
Nik

Hi again!

Since you have not replied back regarding the matter, I will be marking my previous reply as the solution.

Feel free to jump back with any additional information or post again referencing this topic!

Kind Regards,
Nik

Hello @nik.baleca ,

We tried testing with the suggested changes. Errors did occur, but we did not notice any pattern that could help us localize the issue. That said, we would like to get more details about the error we are seeing.

Could you help us with the following:

  • Can Auth0 logs tell us why a specific token was rejected?
  • Are there known issues with useEphemeralSession() and refresh tokens on iOS?
  • Are there specific Dashboard log events we should be watching?

Thanks!

Any help is appreciated!

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

  1. Navigate to Auth0 Dashboard → Applications → Your Application → Settings → Advanced → Refresh Token Rotation
  2. Toggle off "Allow Refresh Token Rotation"
  3. 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:

  1. Navigate to Auth0 Dashboard → Applications → Your Application → Settings → Advanced → Refresh Token Rotation
  2. Enable "Allow Refresh Token Rotation"
  3. Set Rotation Overlap Period to 10–30 seconds (instead of 0)
  4. 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:

  1. Navigate to Auth0 Dashboard → Logs
  2. Search for the user experiencing the error
  3. 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)
  4. 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
  5. 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:

  1. For iOS: Update to the latest version of Auth0.swift
  2. 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:

  1. User calls credentialsManager.credentials() to get a token
  2. Auth0 checks if the access token is expired or near expiration
  3. If expired, Auth0 exchanges the refresh token for a new access token AND a new refresh token
  4. The old refresh token is immediately invalidated (or after the overlap period expires)
  5. 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