Android - Retrofit 2 Refresh Access Token with OkHttpClient and Authenticator

One of the most populars HTTP Client for Android is Retrofit. When calling API, we may require authentication using token. Usually, the token is expired after certain amount of time and needs to be refreshed using refresh token. The client would need to send an additional HTTP request in order to get the new token. Imagine you have a collection of many different APIs, each of them require token authentication. If you have to handle refresh token by modifying your code one by one, it will take a lot of time and of course it's not a good solution. In this tutorial, I'm going to show you how to handle refresh token on each API calls automatically if the token expires.

Define The Service

First we define the service. To make it simple, there are only two endpoints. The first one is for getting item list and we assume it requires token authentication. The other endpoint is for getting a new token. If a request to /api/items because of token invalid, it will try to call /api/auth/token in order to get a new token.

MyService.java

  public interface MyService {
      @GET("/api/items")
      Call<List> getItems();
  
      @POST("/api/auth/token")
      @FormUrlEncoded
      Call refreshToken(
              @Field("username") String username,
              @Field("refreshToken") String refreshToken
      );
  }

Implement Custom Authenticator and OkHttpClient

When building a Retrofit instance, you can set an OkHttpClient instance. The OkHttpClient itself is configurable. For this case, we're going to set a custom authenticator for the OkHttpClient. By doing so, the OkHttpClient will try to execute the authenticator's authenticate method if a request failed because of unauthorized.

Inside the authenticate method, it calls the service's refreshToken method which requires the client to pass the refresh token. In this example, the refresh token is stored in SharedPreference. If successful, it will return an okhttp3.Response instance whose Authorization header has been set with the new token obtained from the response.

The challange is we have to use the same instance of MyService in TokenAuthenticator and in the code where getItems is called. The simplest solution is by creating a holder which holds the instance of MyService.

Holder.java

  import android.support.annotation.Nullable;
  
  public class MyServiceHolder {
      MyService myService = null;
  
      @Nullable
      public MyService get() {
          return myService;
      }
  
      public void set(MyService myService) {
          this.myService = myService;
      }
  }

Below is a custom Authenticator based on the explaination above.

TokenAuthenticator.java

  package com.woolha.example.network;
  
  import android.content.Context;
  import android.content.SharedPreferences;
  
  import com.woolha.example.R;
import com.woolha.example.network.dto.RefreshTokenResponse;
import com.woolha.example.network.services.MyServiceHolder;
import java.io.IOException; import okhttp3.Authenticator; import okhttp3.Request; import okhttp3.Response; import okhttp3.Route; public class TokenAuthenticator implements Authenticator { private Context context; private MyServiceHolder myServiceHolder;
public TokenAuthenticator(Context context, MyServiceHolder myServiceHolder) {
this.context = context; this.myServiceHolder = myServiceHolder; } @Override public Request authenticate(Route route, Response response) throws IOException { if (myServiceHolder == null) { return null; } SharedPreferences settings = context.getSharedPreferences(context.getResources() .getString(R.string.sharedPreferences_token), context.MODE_PRIVATE); String refreshToken = settings.getString("refreshToken", null); String username = settings.getString("username", null); retrofit2.Response retrofitResponse = myServiceHolder.get().refreshToken(username, refreshToken).execute(); if (retrofitResponse != null) { RefreshTokenResponse refreshTokenResponse = retrofitResponse.body(); String newAccessToken = refreshTokenResponse.getData().getToken(); return response.request().newBuilder() .header("Authorization", newAccessToken) .build(); } return null; } }

Below is the example of a class for creating custom OkHttpClient instance which uses Builder design pattern. Before requesting for a new token, we need to use the existing token first on the Authorization header. In addition, sometimes we need to send other additional headers. To make the following class flexible, it would be better if we can set any headers dynamically - that's why I also created addHeader method.

For getting a new token, we need to set the authenticator with the instance of TokenAuthenticator, by adding okHttpClientBuilder.authenticator(authenticator); By doing so, the authenticator will be called automatically if the request returns unauthorized status code.

OkHttpClientInstance.java

  package com.woolha.example.network;
import android.content.Context; import android.content.SharedPreferences; import com.woolha.example.R;
import com.woolha.example.network.services.MyServiceHolder;
import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.TimeUnit; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class OkHttpClientInstance { public static class Builder { private HashMap<String, String> headers = new HashMap<>(); private Context context; private MyServiceHolder myServiceHolder; public Builder(Context context, MyServiceHolder myServiceHolder) { this.context = context; this.myServiceHolder = myServiceHolder; } public Builder addHeader(String key, String value) { headers.put(key, value); return this; } public OkHttpClient build() { TokenAuthenticator authenticator = new TokenAuthenticator(context, myServiceHolder); OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder() .addInterceptor( new Interceptor() { @Override public Response intercept(Interceptor.Chain chain) throws IOException { // Add default headers Request.Builder requestBuilder = chain.request().newBuilder() .addHeader("accept", "*/*") .addHeader("accept-encoding:gzip", "gzip, deflate") .addHeader("accept-language", "en-US,en;q=0.9"); // Add additional headers Iterator it = headers.entrySet().iterator(); for (Map.Entry<String, String> entry : headers.entrySet()) { if (entry.getKey() != null && entry.getValue() != null) { requestBuilder.addHeader(entry.getKey(), entry.getValue()); } } if (context != null) { SharedPreferences settings = context.getSharedPreferences(context.getResources() .getString(R.string.sharedPreferences_token), context.MODE_PRIVATE); String token = settings.getString("token", null); if (token != null) { requestBuilder.addHeader("Authorization", token); } } return chain.proceed(requestBuilder.build()); } } ) .connectTimeout(20, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS); okHttpClientBuilder.authenticator(authenticator); return okHttpClientBuilder.build(); } } }

Usage

On the code where we want to call getItems, first create an instance of MyServiceHolder. Then, create an instance of OkHttpClient and by passing the holder. Next, we create an instance of MyService and set it to the MyServiceHolder instance.

Fragment.java

  MyServiceHolder myServiceHolder = new MyServiceHolder();
  
  OkHttpClient okHttpClient = new OkHttpClientInstance.Builder(getActivity(), myServiceHolder)
          .addHeader("Authorization", token)
          .build();
  
  MyService myService = new retrofit2.Retrofit.Builder()
          .baseUrl(BASE_URL)
          .addConverterFactory(GsonConverterFactory.create())
          .client(okHttpClientInstance)
          .build()
          .create(MyService.class);
  
  myServiceHolder.set(myService);
  
  Call<List<Item>> call = myService.getItems();
  
  call.enqueue(new Callback<List<Item>>() {
      @Override
      public void onResponse(final Call<List<Item>> call, final Response<List> response) {
          categoryOptions = response.body();
  
          itemCategoryOptionsAdapter = new ItemCategoryOptionsAdapter(
                  view.getContext(),
                  android.R.layout.simple_spinner_dropdown_item,
                  categoryOptions
          );
  
          spnCategory.setAdapter(itemCategoryOptionsAdapter);
  
          stopLoading();
      }
  
      @Override
      public void onFailure(final Call<List<Item>> call, final Throwable t) {
          String message = t.getMessage();
          Toast.makeText(getContext(), R.string.item_getCategories_failed,
                  Toast.LENGTH_LONG).show();
  
          done();
      }
  });

That's how to implement and use Authenticator in Retrofit for refreshing expired token automatically. If you need to use custom GSON Converter for parsing response using Retrofit, you can read this post.