Okhttp Authenticator multithreading Okhttp Authenticator multithreading multithreading multithreading

Okhttp Authenticator multithreading


  1. Use a singleton Authenticator

  2. Make sure the method you use to manipulate the token is Synchronized

  3. Count the number of retries to prevent excessive numbers of refreshtoken calls

  4. Make sure the API calls to get a fresh token and thelocal storage transactions to save the new token in your local stores are not asynchronous. Or if you want to make them asynchronous make sure you to you token related stuff after they are completed.
  5. Check if the access token is refreshed by another thread already toavoid requesting a new access token from back-end

Here is a sample in Kotlin

@SingleTonclass TokenAuthenticator @Inject constructor(    private val tokenRepository: TokenRepository) : Authenticator {    override fun authenticate(route: Route?, response: Response): Request? {        return if (isRequestRequiresAuth(response)) {            val request = response.request()            authenticateRequestUsingFreshAccessToken(request, retryCount(request) + 1)        } else {            null        }    }    private fun retryCount(request: Request): Int =        request.header("RetryCount")?.toInt() ?: 0    @Synchronized    private fun authenticateRequestUsingFreshAccessToken(        request: Request,        retryCount: Int    ): Request? {        if (retryCount > 2) return null        tokenRepository.getAccessToken()?.let { lastSavedAccessToken ->            val accessTokenOfRequest = request.header("Authorization") // Some string manipulation needed here to get the token if you have a Bearer token            if (accessTokenOfRequest != lastSavedAccessToken) {                return getNewRequest(request, retryCount, lastSavedAccessToken)            }        }        tokenRepository.getFreshAccessToken()?.let { freshAccessToken ->            return getNewRequest(request, retryCount, freshAccessToken)        }        return null    }    private fun getNewRequest(request: Request, retryCount: Int, accessToken: String): Request {        return request.newBuilder()            .header("Authorization", "Bearer " + accessToken)            .header("RetryCount", "$retryCount")            .build()    }    private fun isRequestRequiresAuth(response: Response): Boolean {        val header = response.request().header("Authorization")        return header != null && header.startsWith("Bearer ")    }}


I see here two scenarios based on how API which you call works.

First one is definitely easier to handle - calling new credentials (e.g. access token) doesn't expire old one. To achieve it you can add an extra flag to your credentials to say that credentials are being refreshed. When you got 401 response, you set flag to true, make a request to get new credentials and you save them only if flag equals true so only first response will be handled and rest of them will be ignored. Make sure that your access to flag is synchronized.

Another scenario is a little bit more tricky - every time when you call new credentials old one are set to be expired by server side. To handle it you I would introduce new object to be used as a semafore - it would be blocked every time when 'credentials are being refreshed'. To make sure that you'll make only one 'refresh credentials' call, you need to call it in block of code which is synchronized with flag. It can look like it:

synchronized(stateObject) {   if(!stateObject.isBeingRefreshed) return;   Response response = client.execute(request);   apiClient.setCredentials(response.getNewCredentials());   stateObject.isBeingRefreshed = false;}

As you've noticed there is an extra check if(!stateObject.isBeingRefreshed) return; to cancel requesting new credentials by following requests which received 401 response.


This is my solution to make sure to refresh token only once in a multi-threading case, using okhttp3.Authenticator:

class Reauthenticator : Authenticator {    override fun authenticate(route: Route?, response: Response?): Request? {        if (response == null) return null        val originalRequest = response.request()        if (originalRequest.header("Authorization") != null) return null // Already failed to authenticate        if (!isTokenValid()) { // Check if token is saved locally            synchronized(this) {                if (!isTokenValid()) { // Double check if another thread already saved a token locally                    val jwt = retrieveToken() // HTTP call to get token                    saveToken(jwt)                }            }        }        return originalRequest.newBuilder()                .header("Authorization", getToken())                .build()    }}

You can even write a unit test for this case, too! 🎉