package com.emmaguy.todayilearned.refresh; import android.support.annotation.NonNull; import com.emmaguy.todayilearned.sharedlib.Constants; import com.emmaguy.todayilearned.storage.TokenStorage; import com.squareup.okhttp.Interceptor; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import java.io.IOException; import java.net.HttpURLConnection; import java.util.UUID; import retrofit.RetrofitError; /** * Refresh a token - transparently to rest of the code. Will block a request whilst doing the token refresh, * then continue with the original request once we have a valid token again. * <p> * Created by emma on 14/06/15. */ public class TokenRefreshInterceptor implements Interceptor { private static final String BEARER_FORMAT = "bearer %s"; private final TokenStorage mTokenStorage; private final RedditAuthenticationService mAuthenticationService; public TokenRefreshInterceptor(TokenStorage tokenStorage, RedditAuthenticationService authenticationService) { mTokenStorage = tokenStorage; mAuthenticationService = authenticationService; } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response; if (request.url().toString().contains("access_token")) { // if we're trying to get the access token, carry on response = chain.proceed(request); } else if (mTokenStorage.hasNoToken()) { // User hasn't logged in, request an app only token response = requestAppOnlyTokenAndProceed(chain, request); } else if (mTokenStorage.hasTokenExpired()) { // Token's expired, renew it first response = renewTokenAndProceed(chain, request); } else { response = makeRequest(chain, request); } return response; } // synchronized so we only renew one request at a time @NonNull private synchronized Response requestAppOnlyTokenAndProceed(Chain chain, Request originalRequest) throws IOException { try { Token token = mAuthenticationService.appOnlyToken(Constants.GRANT_TYPE_INSTALLED_CLIENT, UUID.randomUUID().toString()); mTokenStorage.updateToken(token); } catch (RetrofitError error) { if (error.getResponse() == null || isServerError(error.getResponse())) { throw new RuntimeException( "Failed to retrieve app only token, empty response/server error: " + error.getCause()); } else { throw new RuntimeException("Failed to retrieve app only token, unknown cause: " + error .getCause()); } } return addHeaderAndProceedWithChain(chain, originalRequest); } @NonNull private synchronized Response renewTokenAndProceed(Chain chain, Request originalRequest) throws IOException { if (mTokenStorage.hasTokenExpired()) { try { Token token = mAuthenticationService.refreshToken(Constants.GRANT_TYPE_REFRESH_TOKEN, mTokenStorage.getRefreshToken()); mTokenStorage.updateToken(token); } catch (RetrofitError error) { if (error.getResponse() == null || isServerError(error.getResponse())) { throw new RuntimeException( "Failed to renew token, empty response/server error: " + error.getCause()); } else { throw new RuntimeException("Failed to renew token, unknown cause: " + error.getCause()); } } } return addHeaderAndProceedWithChain(chain, originalRequest); } @NonNull private Response makeRequest(Chain chain, Request request) throws IOException { Response r = addHeaderAndProceedWithChain(chain, request); if (r.code() == HttpURLConnection.HTTP_FORBIDDEN || r.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { mTokenStorage.forceExpireToken(); // throw an IOException, so that this request will be retried throw new IOException("Token problem, throwing to retry"); } return r; } @NonNull private Response addHeaderAndProceedWithChain(Chain chain, Request originalRequest) throws IOException { final String value = String.format(BEARER_FORMAT, mTokenStorage.getAccessToken()); final Request authenticatedRequest = originalRequest.newBuilder() .header(Constants.AUTHORIZATION, value) .build(); return chain.proceed(authenticatedRequest); } private boolean isServerError(retrofit.client.Response response) { return response.getStatus() == HttpURLConnection.HTTP_NOT_FOUND || response.getStatus() == HttpURLConnection.HTTP_INTERNAL_ERROR; } }