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;
}
}
In the code above, RefreshTokenResponse
is a class that represents the refresh token response body from the server. That means you need to adjust the class according to the response you need to handle. Below is an example of the refresh token response class.
package com.woolha.example;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class RefreshTokenResponse {
private boolean success;
private RefreshTokenResponseData data;
@Getter
@Setter
@Builder
public class RefreshTokenResponseData {
private String token;
private SignInResponseUser user;
}
}
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 the 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
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.