I'm using Retrofit to handle the communication with the server API, the API user JSON Web Tokens for authentication. The token expires from time to time, and i'm looking for the best way to implement a Retrofit Client that can refresh the token automatically when it expires.
This is the initial implementation i came up with, :
/**
* Client implementation that refreshes JSON WebToken automatically if
* the response contains a 401 header, has there may be simultaneous calls to execute method
* the refreshToken is synchronized to avoid multiple login calls.
*/
public class RefreshTokenClient extends OkClient {
private static final int UNAUTHENTICATED = 401;
/**
* Application context
*/
private Application mContext;
public RefreshTokenClient(OkHttpClient client, Application application) {
super(client);
mContext = application;
}
@Override
public Response execute(Request request) throws IOException {
Timber.d("Execute request: " + request.getMethod() + " - " + request.getUrl());
//Make the request and check for 401 header
Response response = super.execute( request );
Timber.d("Headers: "+ request.getHeaders());
//If we received a 401 header, and we have a token, it's most likely that
//the token we have has expired
if(response.getStatus() == UNAUTHENTICATED && hasToken()) {
Timber.d("Received 401 from server awaiting");
//Clear the token
clearToken();
//Gets a new token
refreshToken(request);
//Update token in the request
Timber.d("Make the call again with the new token");
//Makes the call again
return super.execute(rebuildRequest(request));
}
return response;
}
/**
* Rebuilds the request to be executed, overrides the headers with the new token
* @param request
* @return new request to be made
*/
private Request rebuildRequest(Request request){
List<Header> newHeaders = new ArrayList<>();
for( Header h : request.getHeaders() ){
if(!h.getName().equals(Constants.Headers.USER_TOKEN)){
newHeaders.add(h);
}
}
newHeaders.add(new Header(Constants.Headers.USER_TOKEN,getToken()));
newHeaders = Collections.unmodifiableList(newHeaders);
Request r = new Request(
request.getMethod(),
request.getUrl(),
newHeaders,
request.getBody()
);
Timber.d("Request url: "+r.getUrl());
Timber.d("Request new headers: "+r.getHeaders());
return r;
}
/**
* Do we have a token
*/
private boolean hasToken(){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
return prefs.contains(Constants.TOKEN);
}
/**
* Clear token
*/
private void clearToken(){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
prefs.edit().remove(Constants.TOKEN).commit();
}
/**
* Saves token is prefs
*/
private void saveToken(String token){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
prefs.edit().putString(Constants.TOKEN, token).commit();
Timber.d("Saved new token: " + token);
}
/**
* Gets token
*/
private String getToken(){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
return prefs.getString(Constants.TOKEN,"");
}
/**
* Refreshes the token by making login again,
* //TODO implement refresh token endpoint, instead of making another login call
*/
private synchronized void refreshToken(Request oldRequest) throws IOException{
//We already have a token, it means a refresh call has already been made, get out
if(hasToken()) return;
Timber.d("We are going to refresh token");
//Get credentials
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
String email = prefs.getString(Constants.EMAIL, "");
String password = prefs.getString(Constants.PASSWORD, "");
//Login again
com.app.bubbles.model.pojos.Response<Login> res = ((App) mContext).getApi().login(
new com.app.bubbles.model.pojos.Request<>(credentials)
);
//Save token in prefs
saveToken(res.data.getTokenContainer().getToken());
Timber.d("Token refreshed");
}
}
I don't know the architecture of Retrofit/OkHttpClient deeply, but as far i understand the execute method can be called multiple times from multiple threads, the OkClient
is the same shared between Calls
only a shallow copy is done.
I'm using synchronized
in refreshToken()
method to avoid multiple threads to enter in refreshToken()
and make multiple login calls, i a refresh is needed only one thread should make the refreshCall and the others will use the renewed token.
I haven´t tested it seriously yet, but for what i can see it's working fine. Maybe someone had this problem already and can share his solution, or it can be helpful for someone with the same/similar problem.
Thanks.
For anyone that find this, you should go with OkHttp Interceptors or use the Authenticator API
This is a sample from Retrofit GitHub page
public void setup() {
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new TokenInterceptor(tokenManager));
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.baseUrl("http://localhost")
.build();
}
private static class TokenInterceptor implements Interceptor {
private final TokenManager mTokenManager;
private TokenInterceptor(TokenManager tokenManager) {
mTokenManager = tokenManager;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request initialRequest = chain.request();
Request modifiedRequest = request;
if (mTokenManager.hasToken()) {
modifiedRequest = request.newBuilder()
.addHeader("USER_TOKEN", mTokenManager.getToken())
.build();
}
Response response = chain.proceed(modifiedRequest);
boolean unauthorized = response.code() == 401;
if (unauthorized) {
mTokenManager.clearToken();
String newToken = mTokenManager.refreshToken();
modifiedRequest = request.newBuilder()
.addHeader("USER_TOKEN", mTokenManager.getToken())
.build();
return chain.proceed(modifiedRequest);
}
return response;
}
}
interface TokenManager {
String getToken();
boolean hasToken();
void clearToken();
String refreshToken();
}
If you want to block requests until the authentication is done, you can use the same synchronization mechanism i did in my answer because interceptors can run concurrently on multiple threads
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With