androidretrofit2android-authenticator

Handling access token expiry when internet is unstable


I am using Retrofit2 as the HTTP client for Android. The app uses access-token and refresh-token for communicating with the backend.

Scenario 1: The access-token is expired, the api call fails with 401 error code, which then I use Authenticator for using the refresh-token to get a new pair of access-token and refresh-token and continue with new access-token for the failed call. This all works well with stable internet connection

Scenario 2: Now consider, the internet is not stable and keeps dropping on and off. The internet is there, api call fails due to expired access-token, app makes a login call with refresh-token to get a new pair of access-token and refresh-token. But before I get success 200 for the login call internet drops off. Now internet is back and my app starts to fire the api call again and get 401, then uses refresh token to fire a new login call. Here is the issue, the backend has updated the new access and refresh tokens, but app never received it as the internet went off. Due to which the refresh token I had is no more valid (as a new pair is already created in backend and flushed the old one)

Not sure how I am suppose to handle this scenario ? Here is my authenticator class:

public class MyAuthenticator implements Authenticator {

Context context;

public MyAuthenticator(Context context) {
    this.context = context;
}

@Nullable
@Override
public Request authenticate(Route route, Response response) throws IOException {

    Retrofit client = new Retrofit.Builder()
            .baseUrl(Manager.API_BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    ApiService service = client.create(ApiService.class);

    SharedPreferenceManager sharedPreferenceManager = new SharedPreferenceManager(context);
    String refreshToken = sharedPreferenceManager.getRefreshToken(context);

    AuthRequest authRequest = new AuthRequest();
    authRequest.grant_type = "refresh_token";
    authRequest.refresh_token = refreshToken;

    Call<authResponse> refreshTokenResult = service.refreshToken(authRequest);
    retrofit2.Response accessTokenResponse = refreshTokenResult.execute();
    //check if response equals 400 , mean empty response
    if (accessTokenResponse.isSuccessful()) {
        AuthResponse refreshResult  = (AuthResponse) accessTokenResponse.body();
        //save new access and refresh token
        sharedPreferenceManager.writeLoginResponse(context, refreshResult);
        // then create a new request and modify it accordingly using the new token
        return response.request().newBuilder()
                .header("Authorization", "Bearer " + refreshResult.access_token)
                .build();

    } else {
        LoginActivity_.intent(context).flags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK).start();
        return null;

    }
}

}

Not sure what I am missing and how should I go about this problem.


Solution

  • You can't do anything about this on the client side.

    What you are trying to do is exactly-once delivery of the new tokens, which is impossible. Instead you should design for at-least-once delivery.

    You have to prepare the server for this scenario, i.e. it should accept the old refresh token as well, until it receives the new access token OR refresh token at least once.

    Until then, the server cannot be sure that the client received the new tokens.