Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Retrofit2 Refresh Oauth 2 Token

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!

like image 521
Yasin Kaçmaz Avatar asked Feb 19 '16 23:02

Yasin Kaçmaz


Video Answer


2 Answers

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.

like image 162
Yasin Kaçmaz Avatar answered Sep 21 '22 22:09

Yasin Kaçmaz


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);   } 
like image 26
Ravindra Shekhawat Avatar answered Sep 22 '22 22:09

Ravindra Shekhawat