Android app is reporting as logged in when authentication fails, and Failed token exchange

Hi!

I have multiple tenants in production for different regions. We’ve noticed Android in the UK has started failing for some users. The credential token fails to authenticate when we verify it on our BackEnd.

I’m also seeing some odd errors in the log, that, I believe, are related.

```
{
“date”: “2026-05-19T04:26:33.389Z”,
“type”: “fertft”,
“description”: “Token could not be decoded or is missing in DB”,
“connection_id”: “”,
“client_id”: “”,
“client_name”: “”,
“ip”: “”,
“client_ip”: “”,
“user_agent”: “okhttp 4.12.0 / Other 0.0.0”,
“details”: {
“policy_used”: null
},
“hostname”: “”,
“user_id”: “”,
“user_name”: “”,
“audience”: null,
“scope”: null,
“auth0_client”: {
“name”: “Auth0.Android”,
“env”: {
“android”: “30”
},
“version”: “3.8.0”
},
“location_info”: {
“latitude”: ,
“longitude”: ,
“country_code”: “”,
“country_name”: “”,
“city_name”: “”,
“subdivision_code”: “”,
“subdivision_name”: “”,
“continent_code”: “NA”,
“time_zone”: “America/New_York”,
“country_code3”: “USA”
},
“$event_schema”: {
“version”: “1.0.0”
},
“environment_name”: “prod-uk-1”,
“log_id”: “90020260519042633438927000000000000001223372042014800925”,
“tenant_name”: “”,
“_id”: “90020260519042633438927000000000000001223372042014800925”,
“isMobile”: false,
“id”: “90020260519042633438927000000000000001223372042014800925”
}
```
I’m unsure where to troubelshoot.

I’ve seen past solutions around making sure the application is marked as native, and it is.

this also seems to be limted to android. (Given that it’s only ever using the okhttp library, which is the kotlin/android networking library.)

Hi @techadmin2

Welcome back to the Auth0 Community!

The fertft error code stands for Failed Exchange of Refresh Token for Access Token . The specific description "Token could not be decoded or is missing in DB" almost always indicates that Auth0’s security mechanisms proactively wiped the user’s Refresh Token family from the database due to a perceived Refresh Token Reuse . On Android, this is most commonly caused by network-level retries occurring on spotty mobile connections.

When you have Refresh Token Rotation enabled, every time the Android app uses a Refresh Token to get a new Access Token, Auth0 issues a new Refresh Token and invalidates the old one.

If Auth0 receives the same Refresh Token twice, it assumes a malicious replay attack is occurring. To protect the user, Auth0 immediately revokes the entire token family. Any subsequent attempts by the app to use that token will result in the exact error you are seeing: the token is “missing in the DB”.

Given this is limited to Android and localized to a specific geographic region, here are the most likely sequence of events:

  1. A user in the UK on a spotty mobile data connection attempts to open the app, triggering a token refresh.
  2. The request successfully reaches your Auth0 UK tenant. Auth0 generates the new tokens, updates the database, and sends the HTTP response back.
  3. The network connection drops before the Android device receives the response. Because the Android networking library often has silent connection retries enabled by default, or the app’s internal logic attempts a retry, it fires the exact same request containing the old Refresh Token.
  4. Auth0 receives the old token, flags it as “Reused,” and immediately deletes the token family from the database.
  5. The next time the app tries to refresh, you get the fertft log.

Because the token family has been completely destroyed on the backend, this state is unrecoverable for the affected session. The token cannot be “fixed.”

To resolve this, you must implement a robust catch-and-recover mechanism in your Android codebase. You must ensure your Android application catches the specific authentication exception and routes the user back to the Universal Login screen. If you are using the CredentialsManager in the Auth0.Android SDK, it should look like this:

credentialsManager.getCredentials(object : Callback<Credentials, CredentialsManagerException> {
    override fun onSuccess(credentials: Credentials) {
    }

    override fun onFailure(error: CredentialsManagerException) {
        if (error.isInvalidRefreshToken) {
            credentialsManager.clearCredentials()
            loginUser() 
        }
    }
})

Ensure you are relying strictly on the thread-safe CredentialsManager.getCredentials() method to fetch your tokens before API calls. Do not manually extract the Refresh Token and attempt to hit the /oauth/token endpoint yourself, as managing the concurrency to prevent race conditions across Android coroutines/threads is highly error-prone.

If you have any other questions, please let me know!

Kind Regards,
Nik

Thanks for the help, I believe we already are taking your recommended steps.

What this doesn’t explain is why this is regional?

EDIT:

The code in question

    override fun isUserLoggedIn(): Boolean {
        val auth0 = getAuth0APIForRegion() ?: return false
        val credentialsManager = SecureCredentialsManager(
            context = ctx,
            auth0 = auth0,
            storage = SharedPreferencesStorage(ctx)
        )
        return credentialsManager.hasValidCredentials()
    }

This is run at app startup, and is reporting true, and when we then get the auth token with the following:

    override suspend fun getAuthToken(): String {
        val auth0 = getAuth0APIForRegion() ?: return ""
        val credentialsManager = SecureCredentialsManager(
            context = ctx,
            auth0 = auth0,
            storage = SharedPreferencesStorage(ctx)
        )
        return suspendCoroutine { cont ->
            credentialsManager.getCredentials(object :
                Callback<Credentials, CredentialsManagerException> {
                override fun onFailure(error: CredentialsManagerException) {
                    Sentry.captureException(error)
                    cont.resume("")
                }

                override fun onSuccess(result: Credentials) {
                    cont.resume(result.accessToken)
                }
            })
        }
    }

Sentry is not reporting errors here.

So we are indeed already logging the recommendations above, and are not seeing errors. until the API is actually called.

Thanks for providing more context regarding the Failed Exchange of Refresh Token for Access Token (fertft) issue that you are experiencing.

From analyzing the code that you have sent, I believe that the root cause of the fertft (Refresh Token Reuse) error is that SecureCredentialsManager is being instantiated repeatedly inside your functions instead of being used as a Singleton.

By creating a new instance of SecureCredentialsManager every time getAuthToken() is called, you are bypassing the SDK’s built-in thread-safety mechanisms. This allows multiple concurrent API calls at app startup to fire multiple token refresh requests to Auth0 simultaneously using the exact same Refresh Token, triggering the security revocation.

The reason why this could be only a regional issue with you current implementation is that race conditions are hyper-sensitive to network latency and timing jitter.
If your UK users are connecting to a newly provisioned UK tenant, the geographical routing, DNS resolution times, and API response latencies are different than your US or EU tenants. If the UK network latency aligns perfectly with the time it takes your Android coroutines to dispatch those concurrent API calls, the race condition will trigger flawlessly. It does not appear to be a bug in the UK tenant; the UK network conditions are just the perfect storm for this race condition to manifest.

In order to attempt to fix this issue, you must ensure that SecureCredentialsManager is a Singleton across the entire lifecycle of your application:

  1. Refactor using Dependency Injection (e.g., Hilt/Dagger):
    If you use DI, provide the SecureCredentialsManager as a @Singleton .

2.Refactor using a Kotlin Object / Lazy initialization: If you aren’t using DI, initialize it once globally:

object AuthManager {
    private var credentialsManager: SecureCredentialsManager? = null

    fun getManager(context: Context): SecureCredentialsManager {
        if (credentialsManager == null) {
            val auth0 = getAuth0APIForRegion() // Ensure this returns correctly
            credentialsManager = SecureCredentialsManager(
                context,
                auth0,
                SharedPreferencesStorage(context)
            )
        }
        return credentialsManager!!
    }
}

  1. Then update your methods to use the shared instance:
override suspend fun getAuthToken(): String {
    val manager = AuthManager.getManager(ctx)
    
    return suspendCoroutine { cont ->
        manager.getCredentials(object : Callback<Credentials, CredentialsManagerException> {
            override fun onSuccess(result: Credentials) {
                cont.resume(result.accessToken)
            }
            override fun onFailure(error: CredentialsManagerException) {
                Sentry.captureException(error)
                cont.resume("")
            }
        })
    }
}

Looking forward to your next reply regarding if the provided solution to the matter resolved the issue or not.

Kind Regards,
Nik