ticofab ticofab - 1 month ago 12
Android Question

Android OkHttp, refresh expired token

Scenario: I am using OkHttp / Retrofit to access a web service: multiple HTTP requests are sent out at the same time. At some point the auth token expires, and multiple requests will get a 401 response.

Issue: In my first implementation I use an interceptor (here simplified) and each thread tries to refresh the token. This leads to a mess.

public class SignedRequestInterceptor implements Interceptor {

@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();

// 1. sign this request
request = request.newBuilder()
.header(AUTH_HEADER_KEY, BEARER_HEADER_VALUE + token)
.build();


// 2. proceed with the request
Response response = chain.proceed(request);

// 3. check the response: have we got a 401?
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {

// ... try to refresh the token
newToken = mAuthService.refreshAccessToken(..);


// sign the request with the new token and proceed
Request newRequest = request.newBuilder()
.removeHeader(AUTH_HEADER_KEY)
.addHeader(AUTH_HEADER_KEY, BEARER_HEADER_VALUE + newToken.getAccessToken())
.build();

// return the outcome of the newly signed request
response = chain.proceed(newRequest);

}

return response;
}
}


Desired solution: All threads should wait for one single token refresh: the first failing request triggers the refresh, and together with the other requests waits for the new token.

What is a good way to proceed about this? Can some built-in features of OkHttp (like the Authenticator) be of help? Thank you for any hint.

Answer

Thanks for your answers - they led me to the solution. I ended up using a ConditionVariable lock and an AtomicBoolean. Here's how you can achieve this: read through the comments.

/**
 * This class has two tasks:
 * 1) sign requests with the auth token, when available
 * 2) try to refresh a new token
 */
public class SignedRequestInterceptor implements Interceptor {

    // these two static variables serve for the pattern to refresh a token
    private final static ConditionVariable LOCK = new ConditionVariable(true);
    private static final AtomicBoolean mIsRefreshing = new AtomicBoolean(false);

    ...

    @Override
    public Response intercept(@NonNull Chain chain) throws IOException {
        Request request = chain.request();

        // 1. sign this request
        ....

        // 2. proceed with the request
        Response response = chain.proceed(request);

        // 3. check the response: have we got a 401?
        if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {

            if (!TextUtils.isEmpty(token)) {
                /*
                *  Because we send out multiple HTTP requests in parallel, they might all list a 401 at the same time.
                *  Only one of them should refresh the token, because otherwise we'd refresh the same token multiple times
                *  and that is bad. Therefore we have these two static objects, a ConditionVariable and a boolean. The
                *  first thread that gets here closes the ConditionVariable and changes the boolean flag.
                */
                if (mIsRefreshing.compareAndSet(false, true)) {
                    LOCK.close();

                    // we're the first here. let's refresh this token.
                    // it looks like our token isn't valid anymore.
                    mAccountManager.invalidateAuthToken(AuthConsts.ACCOUNT_TYPE, token);

                    // do we have an access token to refresh?
                    String refreshToken = mAccountManager.getUserData(account, HorshaAuthenticator.KEY_REFRESH_TOKEN);

                    if (!TextUtils.isEmpty(refreshToken)) {
                        .... // refresh token
                    }
                    LOCK.open();
                    mIsRefreshing.set(false);
                } else {
                    // Another thread is refreshing the token for us, let's wait for it.
                    boolean conditionOpened = LOCK.block(REFRESH_WAIT_TIMEOUT);

                    // If the next check is false, it means that the timeout expired, that is - the refresh
                    // stuff has failed. The thread in charge of refreshing the token has taken care of
                    // redirecting the user to the login activity.
                    if (conditionOpened) {

                        // another thread has refreshed this for us! thanks!
                        ....
                        // sign the request with the new token and proceed

                        // return the outcome of the newly signed request
                        response = chain.proceed(newRequest);
                    }
                }
            }
        }

        // check if still unauthorized (i.e. refresh failed)
        if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
            ... // clean your access token and prompt user for login again.
        }

        // returning the response to the original request
        return response;
    }
}
Comments