SangsooNam / sangsoonam.github.io

Personal blog powered by Jekyll
http://sangsoonam.github.io
4 stars 4 forks source link

2019/03/06/okhttp-how-to-refresh-access-token-efficiently #6

Open utterances-bot opened 4 years ago

utterances-bot commented 4 years ago

OkHttp: How to Refresh Access Token Efficiently

When you use the token-based authentication including OAuth, there are two tokens: access token and refresh token. Whenever you need to access a protected re...

http://sangsoonam.github.io/2019/03/06/okhttp-how-to-refresh-access-token-efficiently.html

tartarJR commented 4 years ago

Hi Sangsoo

Very detailed article about the topic but I have some questions. what if your updatedAccessToken is somehow returned null from the repository?

tartarJR commented 4 years ago

For example what happens when some error happens while you are trying to refresh the access token?

SangsooNam commented 4 years ago

@tartarJR In my example, accessTokenRepository doesn't return null value. If it fails to get the updated access token, refreshAccessToken throws an exception. It might be due to a temporal unstable network condition. Authenticator approach has the auto-retry function for that. If it fails to get the updated access token anyhow, a request will be failed. When there is another request using the access token, refreshAccessToken will be triggered again since accountRepository has still an expired access token.

panagiac commented 4 years ago

Hi, I'm working on a project which requires Oauth2 authentication. But I don't know why, the Authenticator class doesn't work, everytime I get 401, OkHttp triggers only the Interceptor.

Do you know why? Thanks

ghost commented 4 years ago

What do you do if refresh token is expired? How to cancel request from Authenticator?

SangsooNam commented 4 years ago

@s7aycool As you said, the refresh token can be expired although its expiration time is much longer than access tokens. Or, the refresh token can be invalid when a user revokes the OAuth access. In these cases, we need to send users through an OAuth authorization flow again.

Since the refresh token is not valid anymore, you can't get the new access token successfully. accountRepository.refreshAccessToken() could either throw an exception or null. At that time, you can take some action. One example could be to notify a user login manger to logout so that users can do OAuth authorization flow again.

livnata commented 4 years ago

Hi , I guess that AccessTokenRepository is the place that run the relevant api that request from server the new access token with the refresh token right ? If yes can you show us the AccessTokenRepository class ?

Hesowcharov commented 4 years ago

Thank you for the article! Really, I think it's the most actual and useful information about implementing authentication + refreshing token/secret/whatever according to the new retrofit architecture! Very good notice about side effects when you call many times chain.proceed in your application interceptor. So, you should have 2 things - Authenticator to refresh an expired or invalid token; Interceptor to apply the current known token.

godfather commented 4 years ago

Hi @SangsooNam, thank you very much for the article. I have implemented your solution, but I am having some problems resolving the issue of updating an access token when you are running multiple simultaneous requests.

Theif(!accessToken.equals(newAccessToken)) statement does not seem to be enough, and the refresh call is called multiple times. Do you have any suggestion of how can I implement the Authenticator to work with multiple concorrents requests?

SangsooNam commented 4 years ago

@godfather I haven't tested that case deeply but it seems synchronized(this) doesn't work as expected. The critical section may be not valid for multiple requests. I think synchronizing with an object could help you. e.g.

private final static Object sLock = new Object();
...
synchronized(sLock) {
...
}
gs-ts commented 3 years ago

hi @SangsooNam! The article is very helpful and I use it as an example in my code.

I would like to ask you, in case the repository (the okhttp client) needs a header with the refresh token, in order to refreshAccessToken then how would you add it in the request? The access token is expired, so in order to refresh it, you need to make a request with the refresh token. How are you supposed to do this?

SangsooNam commented 3 years ago

@gs-ts Typically, with scope and grant_type parameters, you first request a token to the backend. The backend returns a response like below.

{
  "token_type":"bearer",
  "access_token":"{ACCESS_TOKEN_VALUE}",
  "expires_in":3600,
  "refresh_token":"{REFRESH_TOKEN_VALUE}"
}

As the refresh token is used to get a renewed access token, you can save it to the persistence layer. Unless this is revoked in the backend, it can be used to refreshAccessToken.

As I mentioned above, AccessTokenAuthenticator will be called when the access token is expired. At that time, the logic is to get a new access token by refreshAccessToken and replace the Authorization header with that new token. Note that I store the new access token when refreshAccessToken is called so that I can use it for upcoming requests.

// Need to refresh an access token
final String updatedAccessToken = accountRepository.refreshAccessToken();
return newRequestWithAccessToken(response.request(), updatedAccessToken);
private Request newRequestWithAccessToken(@NonNull Request request, @NonNull String accessToken) {
    return request.newBuilder()
            .header("Authorization", "Bearer " + accessToken)
            .build();
}
lukasz-kalnik-gcx commented 2 years ago

Hey, thanks for the great article! Can you explain the authenticate() method logic in a bit more detail? Specifically, I don't understand why the access token is refreshed every time when newAccessToken == accessToken. Wouldn't this mean that the access token gets refreshed constantly?

SangsooNam commented 2 years ago

@lukasz-kalnik-gcx First, authenticate() is not called every time. That is only called when the request receives HTTP unauthorized error. That time would be the best time to update the token. Note that it's not necessary to update the token if a request doesn't use the token. Most requests would have the access token in the header but some could not. The below logic checks it.

    final String accessToken = accountRepository.getAccessToken();
    if (!isRequestWithAccessToken(response) || accessToken == null) {
        return null;
    }

This Authenticator could be called whenever there is an unauthorized error. In some cases, multiple threads can call the authenticate method due to failing requests. To avoid multiple updates and race conditions, I use thesynchronizedblock.

    synchronized (this) {

However, accountRepository.refreshAccessToken() can be called in any place and the synchronized block cannot cover those cases. Thus, the below logic checks whether the token was updated within the synchronized block.

        final String newAccessToken = accountRepository.getAccessToken();
        // Access token is refreshed in another thread.
        if (!accessToken.equals(newAccessToken)) {
            return newRequestWithAccessToken(response.request(), newAccessToken);
        }

If the token was not updated, it's time to refresh the token.refreshAccessToken() returns the refreshed token and also changed the stored token so that it will be returned by getAccessToken(). The refreshed token is used for the new request.

        // Need to refresh an access token
        final String updatedAccessToken = accountRepository.refreshAccessToken();
        return newRequestWithAccessToken(response.request(), updatedAccessToken);
lukasz-kalnik-gcx commented 2 years ago

Thank you so much for taking the time and providing a really comprehensive and helpful answer!

My concrete use case is that I have to provide an access token with every API call. I guess in this case an Interceptor is better suited to add the authorization header? Because an Authenticator is called only reactively, that means in my case I would get 401 for every call and then I would have to repeat every call in the Authenticator. So effectively every call would be executed twice, which seems wasteful.

I'm thinking about providing the access token inside an Interceptor for every call. Then I could use the Authenticator only for the case when e.g. the access token expires and then it would refresh the access token, store it for use in the Interceptor and retry the last failed call. Does it sound plausible to you?

SangsooNam commented 2 years ago

@lukasz-kalnik-gcx Yes, and actually that's the way I'm suggesting in this article.

OkHttpClient client = okHttpClient.newBuilder()
  .authenticator(new AccessTokenAuthenticator()) // To update the token when it gets HTTP unauthorized error
  .addInterceptor(new AccessTokenInterceptor()) // To set the token in the header. 
  .build();

As I mentioned in the article, AccessTokenInterceptor does only set the token in the header.

  @Override
  public Response intercept(Chain chain) throws IOException {
    String accessToken = accountRepository.getAccessToken();
    Request request = newRequestWithAccessToken(chain.request(), accessToken);
    return chain.proceed(request);
  }
lukasz-kalnik-gcx commented 2 years ago

Amazing, thanks again for the explanations!

PriyankaMadadi85 commented 2 years ago

Hi, Thanks for the great article. Can you please share the code of AccessTokenRepository class as well I am unable to find java examples for refreshAccessToken using OkHttpClient

Regards, Priyanka

SangsooNam commented 2 years ago

@PriyankaMadadi85 refreshAccessToken can be implemented by calling a refresh token endpoint using OkHttpClient. Here is the simplified example.

String refreshAccessToken() throws IOException {
    Request request = new Request.Builder()
            .url("http://<REFRESH_TOKEN_URL>?refresh_token=" + mRefreshToken)
            .build();

    try (Response response = mOkHttpClient.newCall(request).execute()) {
        String newAccessToken = response.body().string();
        setAccessToken(newAccessToken); // For `getAccessToken` later
        return newAccessToken;
    }
}
Waylon-Firework commented 2 years ago

I met the same issue as @godfather .

I have implemented your solution, but I am having some problems resolving the issue of updating an access token when you are running multiple simultaneous requests.

Theif(!accessToken.equals(newAccessToken)) statement does not seem to be enough, and the refresh call is called multiple times. Do you have any suggestion of how can I implement the Authenticator to work with multiple concorrents requests?
Waylon-Firework commented 2 years ago

Sorry, it works. I forgot to update the newAccessToken. Thanks!

Waylon-Firework commented 2 years ago
// Need to refresh an access token
final String updatedAccessToken = accessTokenRepository.refreshAccessToken();

Is the refreshAccessToken a sync request method? If refreshAccessToken error, is it appropriate to return to the login page here?

SangsooNam commented 2 years ago

@Waylon-Firework In my code, refreshAccessToken itself is not a synchronous method, and I used synchronized block within intercept call in AccessTokenInterceptor to avoid refreshing it by multiple threads. The code snippet doesn't have that part, but I believe it's possible to redirect a user to the login page by capturing the error.

sudeeps-r commented 1 year ago

Hi I have use case where 4-5 api calls firing the auth-interceptor and result in that many refresh token, how does sync block fix this issue?