27

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?

hexonxons
  • 845
  • 1
  • 10
  • 19

6 Answers6

15
  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 ")
    }
}
Tartar
  • 5,149
  • 16
  • 63
  • 104
  • Doesn't this code assume the _original_ request already had an Authorization header before it does anything? Do you require the app code to provide that? – mabi Jun 18 '20 at 01:57
  • @mabi I do not know if that is what you mean but you may try to use an interceptor to distinguish the endpoints that require Authorization header and apply the TokenAuthenticatorfor only those endpoints. – Tartar Jun 22 '20 at 08:29
  • The way I understand this is that OkHttp will call `authenticate` on a 401/403 response, you then test for an Authorization header in `isRequestRequiresAuth`, returning null when there's none. So if the original request doesn't already have an Authorization header you abort the request? – mabi Jun 25 '20 at 20:03
  • 1
    According to the documentation, the authenticate method will react to HTTP 401 unauthorized responses only. If your original request cannot answer the authentication demand then the authenticate method will return null. – Tartar Jun 25 '20 at 21:09
  • `synchronized` is blocking the current thread so it is likely to lead the application to be unresponsive, right? – Jimale Abdi Jan 30 '23 at 18:05
  • It won't block the thread, it will just prevent other threads to write until the current thread is done with its job. – Tartar Feb 15 '23 at 15:48
4

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.

Krzysztof Skrzynecki
  • 2,345
  • 27
  • 39
  • That way, you lose the requests that are returned, isn't it? With the way I put below, you can call that requests again. – antonicg Oct 21 '16 at 08:01
  • Which scenario do you mean? If server doesn't expire old access token when new one is generated, imo you doesn't need to block it and it isn't a problem when we lose any because we'll still have an access token which work. Of course it defends how your system works. In second scenario I described how to prevent invoking more 'refresh access token' requests in the first place (synchronized block will prevent doing it), so nothing will be lost. – Krzysztof Skrzynecki Oct 21 '16 at 12:35
  • That's true, If server doesn't expire old access is no needed to block the old requests. Thanks for the aclaration. – antonicg Oct 24 '16 at 15:15
3

In my case I implemented the Authenticator using the Singleton pattern. You can made synchronized that method authenticate. In his implementation, I check if the token from the request (getting the Request object from Response object received in the params of authenticate method) is the same that the saved in the device (I save the token in a SharedPreferences object).

If the token is the same, that means that it has not been refresed yet, so I execute the token refresh and the current request again.

If the token is not the same, that means that it has been refreshed before, so I execute the request again but using the token saved in the device.

If you need more help, please tell me and I will put some code here.

antonicg
  • 934
  • 10
  • 24
  • 2
    I think it would be good to add info that token needs to be saved by "commit" method instead of "apply" to make it synchronous. – Krzysztof Skrzynecki Oct 21 '16 at 12:39
  • Hi @antonicg. Can you please provide some code? I am trying to find a way to handle instances where different threads may attempt a refresh at the same time. It sounds like your answer could solve my problem, but the synchronization is where I am stuck and don't quite understand. – JPM Nov 13 '17 at 06:14
  • Hi @JPM I don't have this code nowadays. but you have to implement the interface Authenticator on a class and note the method authenticate as synchronized. If you configure that class as Singleton you only have one instance of it and only one thread will access at time to the synchronized method. You set a flag or something where you note when you refreshed the token and you will achieve it. – antonicg Nov 13 '17 at 12:02
  • No worries @antonicg! Only question I have left is are you suggesting I need some lock inside a synchronized authenticate method? I was under the impression as soon as I mark the method as synchronized, I do not need a lock or anything like that since it will pause executions until the current thread exits the method. My authenticator also uses shared preferences so I was thinking your suggestion of comparing the request token with what is saved should be enough? – JPM Nov 14 '17 at 02:38
  • @JPM Yes, you are right, you don't need any lock if you mark the method as synchronized – antonicg Nov 14 '17 at 08:12
2

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!

Sebastian
  • 2,896
  • 23
  • 36
  • What does the function `isTokenValid()` do? Does that hit the server each time to check that a token is still valid? – 11m0 Jun 01 '19 at 15:00
  • Thanks for your question, I understand it is not obvious. It is just a local check and the implementation will depend on your specific case. I've commented the code to make that more clear. – Sebastian Jun 28 '19 at 15:18
  • I'm late to the party, but one question: when your function is called for a request with a bearer token already in it, that means okhttp has tried that request and it failed. Wouldn't you want to fetch a new token in this case rather than letting the request fall to floor? – mabi Jun 18 '20 at 01:54
0

Add synchronized to authenticate() method signature.

And make sure getToken() method is blocking.

@Nullable
@Override
public synchronized Request authenticate(Route route, Response response) {

    String newAccessToken = getToken();

    return response.request().newBuilder()
            .header("Authorization", "Bearer " + newAccessToken)
            .build();
}
researcher
  • 1,758
  • 22
  • 25
0

Make sure to use singleton custom Authenticator When refreshing token successful return request with new token else return null.

class TokenAuthenticator(
private val sharedPref: SharedPref,
private val tokenRefreshApi: TokenRefreshApi

) : Authenticator, SafeApiCall {

override fun authenticate(route: Route?, response: Response): Request? {
    return runBlocking {
        when (val tokenResponse = getUpdatedToken()) {
            is Resource.Success -> {
                val token = tokenResponse.data.token
                sharedPref.saveToken(token)
                response.request.newBuilder().header("Authorization", "Bearer $token").build()
            }
            else -> {
                null
            }
        }
    }
}

private suspend fun getUpdatedToken(): Resource<LoginResponse> {
    return safeApiCall { tokenRefreshApi.refreshToken("Bearer ${sharedPref.getToken()}") }
}

}

Zohidjon Akbarov
  • 3,223
  • 1
  • 9
  • 9