Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrofit custom Client for WebTokens authentication

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 synchronizedin 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.

like image 585
Sergio Serra Avatar asked Oct 03 '15 18:10

Sergio Serra


1 Answers

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

like image 91
Sergio Serra Avatar answered Nov 25 '22 18:11

Sergio Serra