31

I am using Retrofit and OkHttp libraries. I have an Authenticator that authenticates the user when we get a 401 response.

My build.gradle is like this:

compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
compile 'com.squareup.okhttp3:okhttp:3.1.2'

And my Authenticator is like this:

public class CustomAuthanticator  implements Authenticator {
@Override
public Request authenticate(Route route, Response response) throws IOException {
    
    //refresh access token
    refreshTokenResult=apiService.refreshUserToken(parameters);
    //this is synchronous retrofit request
    RefreshTokenResult refreshResult = refreshTokenResult.execute().body();
    //check if response equals 400, means empty response
    if(refreshResult != null) {
        // save new access and refresh token
        // then create a new request and new access token as header
        return response.request().newBuilder()
                .header("Authorization", newaccesstoken)
                .build();

    } else {
        // we got empty response and we should return null
        // if we don't return null
        // this method will try to make so many requests to get new access token
        return null;
    }
                    
}}

This is my APIService class :

public interface APIService {

@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST("token")
public Call<RefreshTokenResult> refreshUserToken(@Header("Accept") String accept, 
    @Header("Content-Type") String contentType, @Field("grant_type") String grantType,
    @Field("client_id") String clientId, @Field("client_secret") String clientSecret, 
    @Field("refresh_token") String refreshToken);
}

I am using Retrofit like this:

CustomAuthanticator customAuthanticator=new CustomAuthanticator();
OkHttpClient okClient = new OkHttpClient.Builder()
        .authenticator(customAuthanticator)
        .build();
Retrofit client = new Retrofit.Builder()
        .baseUrl(getResources().getString(R.string.base_api_url))
        .addConverterFactory(GsonConverterFactory.create(gson))
        .client(okClient)
        .build();
    
//then make retrofit request

So my question is: Sometimes I get a new access token and continue work. But sometimes I get a 400 response which means an empty response. So my old refresh token is invalid and I can't get a new token. Normally our refresh token expires in 1 year. So how I can do this. Please help me!

Yasin Kaçmaz
  • 6,573
  • 5
  • 40
  • 58

2 Answers2

48

Disclaimer : Actually I am using Dagger +RxJava + Retrofit but I just wanted to provide an answer to demonstrate logic for future visitors.

Important : If you are making requests from several places your token will refresh multiple times inside TokenAuthenticator class. For example when your activity and your service make requests concurrently. To beat this issue just add synchronized keyword to your TokenAuthenticators authenticate method.

Please make synchronous requests when refreshing your token inside Authenticator because you must block that thread until your request finishes, otherwise your requests will be executed twice with old and new tokens. You can use Schedulers.trampoline() or blockingGet() when refreshing your token to block that thread.

Also inside authenticate method you can check if token is already refreshed by comparing request token with stored token to prevent unnecessary refresh.

And please do not consider using TokenInterceptor because it is edge case and not for everyone, just focus on TokenAuthenticator.

This is what we are trying to achieve:

enter image description here

First of all refreshing token is a critical process for most apps. The flow is: If refresh token fails, logout current user and require to re-login. (Maybe retry refresh token couple of times before logging out the user)

Anyways I will explain it step by step:

Step 1: Please refer singleton pattern, we will create one class that's responsible for returning our retrofit instance. Since it is static if there is no instance available it just creates instance only once and when you call it always returns this static instance. This is also basic definition of Singleton design pattern.

public class RetrofitClient {

private static Retrofit retrofit = null;

private RetrofitClient() {
    // private constructor to prevent access
    // only way to access: Retrofit client = RetrofitClient.getInstance();
}

public static Retrofit getInstance() {
    if (retrofit == null) {
        // TokenAuthenticator can be singleton too
        TokenAuthenticator tokenAuthenticator = new TokenAuthenticator();

        // !! This interceptor is not required for everyone !!
        // Main purpose of this interceptor is to reduce server calls

        // Our token needs to be refreshed after 10 hours
        // We open our app after 50 hours and try to make a request.
        // Of course token is expired and we will get a 401 response.
        // So this interceptor checks time and refreshes token beforehand.
        // If this fails and I get 401 then my TokenAuthenticator does its job.
        // if my TokenAuthenticator fails too, basically I just logout the user.
        TokenInterceptor tokenInterceptor = new TokenInterceptor();

        OkHttpClient okClient = new OkHttpClient.Builder()
                .authenticator(tokenAuthenticator)
                .addInterceptor(tokenInterceptor)
                .build();

        retrofit = new Retrofit.Builder()
                .baseUrl(base_api_url)
                .client(okClient)
                .build();
    }
    return retrofit;
  }
}

Step 2: In my TokenAuthenticator's authenticate method :

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

    boolean refreshResult = refreshToken();
    if (refreshResult) {
    // refresh token is successful, we saved new token to storage.
    // Get your token from storage and set header
    String newaccesstoken = "your new access token";

    // execute failed request again with new access token
    return response.request().newBuilder()
            .header("Authorization", newaccesstoken)
            .build();

    } else {
        // Refresh token failed, you can logout user or retry couple of times
        // Returning null is critical here, it will stop the current request
        // If you do not return null, you will end up in a loop calling refresh
        return null;
    }
}

And refreshToken method, this is just an example you can create your own:

public boolean refreshToken() {
    // you can use RxJava with Retrofit and add blockingGet
    // it is up to you how to refresh your token
    RefreshTokenResult result = retrofit.refreshToken();
    int responseCode = result.getResponseCode();

    if(responseCode == 200) {
        // save new token to sharedpreferences, storage etc.
        return true;
    } else {
        //cannot refresh
        return false;
    } 
}

Step 3: For those who wants to see TokenInterceptor logic:

public class TokenInterceptor implements Interceptor {
SharedPreferences prefs;
SharedPreferences.Editor prefsEdit;

@Override
public Response intercept(Chain chain) throws IOException {

    Request newRequest = chain.request();

    // get expire time from shared preferences
    long expireTime = prefs.getLong("expiretime",0);
    Calendar c = Calendar.getInstance();
    Date nowDate = c.getTime();
    c.setTimeInMillis(expireTime);
    Date expireDate = c.getTime();

    int result = nowDate.compareTo(expireDate);
    // when comparing dates -1 means date passed so we need to refresh token
    if(result == -1) {
        //refresh token here , and get new access token
        TokenResponse tokenResponse = refreshToken();

        // Save refreshed token's expire time :
        integer expiresIn = tokenResponse.getExpiresIn();
        Calendar c = Calendar.getInstance();
        c.add(Calendar.SECOND,expiresIn);
        prefsEdit.putLong("expiretime",c.getTimeInMillis());

        String newaccessToken = "new access token";
        newRequest=chain.request().newBuilder()
                .header("Authorization", newaccessToken)
                .build();
    }
    return chain.proceed(newRequest);
  }
}

I am making requests at activities and background services. All of them uses the same retrofit instance and I can easily manage access token. Please refer to this answer and try to create your own client. If you still have issues simply comment below, I'll try to help.

Yasin Kaçmaz
  • 6,573
  • 5
  • 40
  • 58
  • 2
    Good answer. Where i can find `TokenInterceptor` class ? – Ibrahim Disouki May 07 '17 at 09:13
  • Dear @IbrahimDisouki I've added a step for `TokenInterceptor` logic. Please checkout and let me know if you have questions. – Yasin Kaçmaz May 07 '17 at 12:11
  • Thanks for your reply. `integer expiresIn=response.getExpiresIn();` Where does the response object come from? – Ibrahim Disouki May 08 '17 at 13:17
  • 1
    When you got a success token refresh response you must save expiretime in order to use it later. So that part of code demonstrating it. – Yasin Kaçmaz May 08 '17 at 13:44
  • Should i use `TokenAuthenticator` or `TokenInterceptor` is enough ? – Ibrahim Disouki May 13 '17 at 13:25
  • It is up to you. `TokenAuthenticator` acts in when you got 401 response since its `Authenticator` and in my case he is last resort. Im using `TokenInterceptor` class which acts in whenever you make request with that `OkHttp` and it controls if token expired before making request( for example in my case my tokens have 3 days valid time, assume user came to app after 10 days) and refreshes token immediately without getting any 401 response. This means you can refresh your token instead of getting 401 response and then refresh. This approach also reduces the amount of requests made to the server – Yasin Kaçmaz May 13 '17 at 13:34
  • 1
    Thank you @YasinKaçmaz for such a descriptive explanation. I am just curious to know where do you store access Token in your app --> I mean which is the safest place. – j10 Jul 28 '17 at 14:51
  • 2
    @jitenshah ah that question I oftenly think at WC. Right now I am using `SharedPreferences` but you can use [Realm](https://github.com/realm/realm-java) with [Encryption](https://github.com/realm/realm-java/tree/master/examples/encryptionExample/src/main/java/io/realm/examples/encryptionexample) or encrypt with `SQLite` and also there is some libraries wraps `SharedPreferences` with an encryption. But I really don't know if this really required and not tested the performance handicap. Anyways maybe google this: "sharedpreferences security" and if you still interested we can always argue this. – Yasin Kaçmaz Jul 28 '17 at 15:12
  • 1
    @jitenshah also you must know `SharedPreferences` is safe at most devices unless device have root or mounted etc. In this scenerio you can warn user like Whatsapp : "you are using rooted device". Also reading this page could be good : [keystore](https://developer.android.com/training/articles/keystore.html). Sorry for second comment but I had character limit. – Yasin Kaçmaz Jul 28 '17 at 15:16
  • 1
    Thank you @YasinKaçmaz. I read about KeyStore (only Android > M is hardware based and safe) . I will checkout the Whatsapp thing (seems interesting). No more discussion here for this topic. I will ping you. – j10 Jul 28 '17 at 15:19
  • @YasinKaçmaz : a question : When if(result==-1) in TokenInterceptor and we need to issue a new request we need to call the server with a completely NEW REQUEST right ? we can not add it to the current request ? So, in TokenInterceptor --> we make a synchronous call to our new /token/renew URL fetch the answer and then do a chain.proceed right ? – j10 Jul 29 '17 at 07:40
  • @jitenshah actually no we never making request unless interceptor passes. Because interceptor acts before your request and we checked time -1 means we must refresh token so we are refreshing token and adding our request a header "Authorization" and now we can make our request. In this scenerio you never get 401 response because we already refreshed our token without getting 401. Still if you get 401 then our Authenticator acts in. – Yasin Kaçmaz Jul 29 '17 at 12:41
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/150469/discussion-between-jitenshah-and-yasin-kacmaz). – j10 Jul 29 '17 at 12:43
  • 3
    This is by far the best answer of this type of questions, others are just pseudocode with a bunch of open questions. – moxi Feb 07 '18 at 09:49
  • Excellent answer! Thanks for such verbose details. This answer actually helped me understand the logic more clearly. – harsh_v Feb 07 '18 at 11:40
  • @YasinKaçmaz Is there a way to stop the request once I got an error from the refresh token call? For example, my token expired, than I call refreshToken and something went wrong with my backend and the call return an 500 error. I don't won't the user to call the original request because I know that his token is invalid. How can I deal with this situation? – Daniel Nazareth Apr 13 '18 at 12:42
  • @DanielNazareth (scenerio for authenticator) basically I just logout the user. Maybe the token expired. But also you can stop all ongoing requests from one singleton dispather via using : `dispatcher.cancelAll()` (scenerio for interceptor) if you return null your request will not continue. If you tell me where you get 500 error I can give you precise answer. – Yasin Kaçmaz Apr 13 '18 at 14:11
  • @YasinKaçmaz in the TokenInterceptor, inside the if(result==-1), I call the refreshToken request. That request returns me a 500 error. If I call the chain.proceed(newRequest) later, I will make the original call with a token that I already know that is expired – Daniel Nazareth Apr 13 '18 at 15:51
  • 1
    @DanielNazareth you can return null - which will stop request- instead of `return chain.proceed(newRequest)` – Yasin Kaçmaz Apr 13 '18 at 22:53
  • @YasinKaçmaz is it okay to create new retrofit object to call refreshToken method? – Gusti Arya Oct 09 '18 at 14:35
  • @GustiArya actually this answer is a bit old. I wrote it from zero without looking my project architecture. So this answer written for demonstration purpose and you can implement it by using your own architecture – Yasin Kaçmaz Oct 09 '18 at 15:13
  • @YasinKaçmaz how do you pass context from activity? – Gusti Arya Oct 10 '18 at 07:38
  • @GustiArya I do not know which part you are asking but Activity context is not must for retrofit etc. You can extend application class and set its context to your dagger graph. So you have an application context in your graph. Inject it whenever wherever you want – Yasin Kaçmaz Oct 10 '18 at 08:54
  • @YasinKaçmaz thanks for the detailed answer, what is the value of userRefreshToken? what's its purpose? – Michael Oct 19 '18 at 14:26
  • @Michael you need to specify your refreshToken value when refreshing your expired token. `Oauth2` Api will send you a accessToken and refreshToken every time you grant a new token or login. – Yasin Kaçmaz Oct 19 '18 at 15:31
  • I did it.But it is requesting token refresh more and more non stop. – Javohir Ruzimurodov May 22 '20 at 08:12
  • 1
    But what should we do when the token is not expired? Should we save and retrieve a token from Shared Preferences when the token is not expired? – Veniamin Jul 31 '20 at 14:19
  • If token is not expired you can do whatever you want. If expired you get 401 and then you can refresh your token. If you are checking time before token it should not refresh token if token time is not passed – Yasin Kaçmaz Aug 03 '20 at 01:37
  • In case working with JWT Nodejs. How u manage refresh tokens? Multiple Tokens => Many Devices for a user. OR only one Refresh Token => each user and one device only . – Roger Aug 03 '20 at 21:10
  • 1
    @Roger I guess you can manage from backend. Client don't care if you have multiple token or not. It cares if you are unauthorized or your token expired or not. If your token works your client works. I hope I explained it clearly. – Yasin Kaçmaz Aug 04 '20 at 07:21
  • @YasinKaçmaz thanks for answer i think alse that cliente not care about tokens, But i guess care about security i supossed. U think that its better that user can "login correct" in many devices or Only one device, like one session at a time – Roger Aug 04 '20 at 13:26
  • Can you please expand more on this answer? Do you have a complete working code? I am encountering a problem in regards with Retrofit + Authenticator + OkHttp, if you use Dagger2 this combo is impossible to resolve. (Yeah you can use Lazy or what not) but on this answer you use `retrofit.refreshToken`. You did not show how you get retrofit inside `TokenAuthenticator`. You instantiated `TokenAuthenticator` first so how did you have retrofit instance if you instantiate your retrofit later? – Neon Warge Apr 22 '22 at 09:34
  • 1
    Hello @NeonWarge, I had another question regarding to this issue on Stackoverflow. Here is my answer: https://stackoverflow.com/a/43946558/5047398 You can checkout that question and answer, it may be helpful – Yasin Kaçmaz Apr 23 '22 at 10:54
  • So if there are three multiple requests at the same time and we use `TokenAuthenticator`, and even if `refreshToken` func is synchronized is it still going to update token three times in turn? – user924 Sep 22 '22 at 10:44
  • @user924 If you block that thread when refreshing token it won't update three times – Yasin Kaçmaz Sep 22 '22 at 13:18
  • how does it work? we can make refresh func synchronized but it still is going to be called three times (at not the same time of course, each request will wait), that's why in the following example the author compares access token to find out if should refresh token or it already was refreshed https://stackoverflow.com/questions/62950438/using-coroutine-runblock-with-the-authenticator-to-handle-401-response-from-retr – user924 Sep 22 '22 at 15:04
  • And actually I only noticed your tip in the question `Also inside authenticate method you can check if token is already refreshed by comparing request token with stored token to prevent unnecessary refresh.`, so as I understand this is it as in that question – user924 Sep 22 '22 at 15:08
  • @user924 This answer is almost 7 years old. I had same kind of comparison that checks stored token and skips refresh process if token is already refreshed. I believe you can implement same kind of mechenism – Yasin Kaçmaz Sep 26 '22 at 07:44
  • @YasinKaçmaz also this solution didn't for work for me until I added `.removeHeader("Authorization")` before `.addHeader("Authorization", ....` – user924 Sep 29 '22 at 15:57
4

In your ApiClient.java class :

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new AuthorizationInterceptor(context))
                .build();

Add TokenManager.java class in your retrofit package

package co.abc.retrofit;

/**
 * Created by ravindrashekhawat on 17/03/17.
 */

public interface TokenManager {
    String getToken();
    boolean hasToken();
    void clearToken();
    String refreshToken();
}

Add Intercepter class in your package with name AuthorizationInterceptor.java

package co.smsmagic.retrofit;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

import com.google.gson.Gson;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;

import co.abc.models.RefreshTokenResponseModel;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.http.Header;

import static co.abc.utils.abcConstants.ACCESS_TOKEN;
import static co.abc.utils.abcConstants.BASE_URL;
import static co.abc.utils.abcConstants.GCM_TOKEN;
import static co.abc.utils.abcConstants.JWT_TOKEN_PREFIX;
import static co.abc.utils.abcConstants.REFRESH_TOKEN;

/**
 * Created by ravindrashekhawat on 21/03/17.
 */

public class AuthorizationInterceptor implements Interceptor {
    private static Retrofit retrofit = null;
    private static String deviceToken;
    private static String accessToken;
    private static String refreshToken;
    private static TokenManager tokenManager;
    private static Context mContext;

    public AuthorizationInterceptor(Context context) {
        this.mContext = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Request modifiedRequest = null;

        tokenManager = new TokenManager() {
            final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);

            @Override
            public String getToken() {

                accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
                return accessToken;
            }

            @Override
            public boolean hasToken() {
                accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
                if (accessToken != null && !accessToken.equals("")) {
                    return true;
                }
                return false;
            }

            @Override
            public void clearToken() {
                sharedPreferences.edit().putString(ACCESS_TOKEN, "").apply();
            }

            @Override
            public String refreshToken() {
                final String accessToken = null;

                RequestBody reqbody = RequestBody.create(null, new byte[0]);
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder()
                        .url(BASE_URL + "refresh")
                        .method("POST", reqbody)
                        .addHeader("Authorization", JWT_TOKEN_PREFIX + refreshToken)
                        .build();

                try {
                    Response response = client.newCall(request).execute();
                    if ((response.code()) == 200) {
                        // Get response
                        String jsonData = response.body().string();

                        Gson gson = new Gson();
                        RefreshTokenResponseModel refreshTokenResponseModel = gson.fromJson(jsonData, RefreshTokenResponseModel.class);
                        if (refreshTokenResponseModel.getRespCode().equals("1")) {
                            sharedPreferences.edit().putString(ACCESS_TOKEN, refreshTokenResponseModel.getResponse()).apply();
                            return refreshTokenResponseModel.getResponse();
                        }

                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return accessToken;
            }
        };

        final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
        deviceToken = sharedPreferences.getString(GCM_TOKEN, "");
        accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
        refreshToken = sharedPreferences.getString(REFRESH_TOKEN, "");

        Response response = chain.proceed(request);
        boolean unauthorized =false;
        if(response.code() == 401 || response.code() == 422){
            unauthorized=true;
        }

        if (unauthorized) {
            tokenManager.clearToken();
            tokenManager.refreshToken();
            accessToken = sharedPreferences.getString(ACCESS_TOKEN, "");
            if(accessToken!=null){
                modifiedRequest = request.newBuilder()
                        .addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
                        .build();
                return chain.proceed(modifiedRequest);
            }
        }
        return response;
    }
}

Note : This is working code for refresh token that I have provided stay calm you just to change some constant except that it will work perfectly.Just try to understand the logic .

In bottom there is logic to call again the same request

 if(accessToken!=null){
                modifiedRequest = request.newBuilder()
                        .addHeader("Authorization", JWT_TOKEN_PREFIX + tokenManager.getToken())
                        .build();
                return chain.proceed(modifiedRequest);
  }
Ravindra Shekhawat
  • 4,275
  • 1
  • 19
  • 26
  • Thats 401 for refresh token failure not 400 .But I have used TokenManage r interface not saying anything wrong with your approach also just there are always many ways to implement same solution.May it help someone – Ravindra Shekhawat Apr 26 '17 at 14:22
  • Yes your approach is good. But you must try to execute your request to see if it returns any 401 response(unauthorized). You can move this logic to OkHttp's Authenticator since it does same thing and you can check token time in your another Interceptor. If time is expired basically you can refresh your token without get any 401 response. – Yasin Kaçmaz Apr 26 '17 at 14:26