Search code examples
androidmultithreadingauthenticationokhttp

Okhttp Authenticator multithreading


I am using OkHttp in my android application with several async requests. All requests require a token to be sent with the header. Sometimes I need to refresh the token using a RefreshToken, so I decided to use OkHttp's Authenticator class.

What will happen when 2 or more async requests get a 401 response code from the server at the same time? Would the Authenticator's authenticate() method be called for each request, or it will only called once for the first request that got a 401?

@Override
public Request authenticate(Proxy proxy, Response response) throws IOException
{                
    return null;
}

How to refresh token only once?


Solution

    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 refresh token calls

    4. Make sure the API calls to get a fresh token and the local 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 to avoid requesting a new access token from back-end

    Here is a sample in Kotlin

    @SingleTon
    class 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 ")
        }
    }