Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android OkHttp, refresh expired token

Tags:

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.

like image 766
ticofab Avatar asked Jun 24 '15 08:06

ticofab


People also ask

How do I get my twitter refresh token?

A refresh token allows an application to obtain a new access token without prompting the user. You can create a refresh token by making a POST request to the following endpoint: https://api.twitter.com/2/oauth2/token You will need to add in the Content-Type of application/x-www-form-urlencoded via a header.

What is refresh token?

A refresh token is a special token that is used to obtain additional access tokens. This allows you to have short-lived access tokens without having to collect credentials every time one expires.


2 Answers

I had the same problem and I managed to solve it using a ReentrantLock.

import java.io.IOException; import java.net.HttpURLConnection; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;  import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; import timber.log.Timber;  public class RefreshTokenInterceptor implements Interceptor {      private Lock lock = new ReentrantLock();      @Override     public Response intercept(Interceptor.Chain chain) throws IOException {          Request request = chain.request();         Response response = chain.proceed(request);          if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {              // first thread will acquire the lock and start the refresh token             if (lock.tryLock()) {                 Timber.i("refresh token thread holds the lock");                  try {                     // this sync call will refresh the token and save it for                      // later use (e.g. sharedPreferences)                     authenticationService.refreshTokenSync();                     Request newRequest = recreateRequestWithNewAccessToken(chain);                     return chain.proceed(newRequest);                 } catch (ServiceException exception) {                     // depending on what you need to do you can logout the user at this                      // point or throw an exception and handle it in your onFailure callback                     return response;                 } finally {                     Timber.i("refresh token finished. release lock");                     lock.unlock();                 }              } else {                 Timber.i("wait for token to be refreshed");                 lock.lock(); // this will block the thread until the thread that is refreshing                               // the token will call .unlock() method                 lock.unlock();                 Timber.i("token refreshed. retry request");                 Request newRequest = recreateRequestWithNewAccessToken(chain);                 return chain.proceed(newRequest);             }         } else {             return response;         }     }      private Request recreateRequestWithNewAccessToken(Chain chain) {         String freshAccessToken = sharedPreferences.getAccessToken();         Timber.d("[freshAccessToken] %s", freshAccessToken);         return chain.request().newBuilder()                 .header("access_token", freshAccessToken)                 .build();     } } 

The main advantage of using this solution is that you can write an unit test using mockito and test it. You will have to enable Mockito Incubating feature for mocking final classes (response from okhttp). Read more about here. The test looks something like this:

@RunWith(MockitoJUnitRunner.class) public class RefreshTokenInterceptorTest {      private static final String FRESH_ACCESS_TOKEN = "fresh_access_token";      @Mock     AuthenticationService authenticationService;      @Mock     RefreshTokenStorage refreshTokenStorage;      @Mock     Interceptor.Chain chain;      @BeforeClass     public static void setup() {         Timber.plant(new Timber.DebugTree() {              @Override             protected void log(int priority, String tag, String message, Throwable t) {                 System.out.println(Thread.currentThread() + " " + message);             }         });     }      @Test     public void refreshTokenInterceptor_works_as_expected() throws IOException, InterruptedException {          Response unauthorizedResponse = createUnauthorizedResponse();         when(chain.proceed((Request) any())).thenReturn(unauthorizedResponse);         when(authenticationService.refreshTokenSync()).thenAnswer(new Answer<Boolean>() {             @Override             public Boolean answer(InvocationOnMock invocation) throws Throwable {                 //refresh token takes some time                 Thread.sleep(10);                 return true;             }         });         when(refreshTokenStorage.getAccessToken()).thenReturn(FRESH_ACCESS_TOKEN);         Request fakeRequest = createFakeRequest();         when(chain.request()).thenReturn(fakeRequest);          final Interceptor interceptor = new RefreshTokenInterceptor(authenticationService, refreshTokenStorage);          Timber.d("5 requests try to refresh token at the same time");         final CountDownLatch countDownLatch5 = new CountDownLatch(5);         for (int i = 0; i < 5; i++) {             new Thread(new Runnable() {                 @Override                 public void run() {                     try {                         interceptor.intercept(chain);                         countDownLatch5.countDown();                     } catch (IOException e) {                         throw new RuntimeException(e);                     }                 }             }).start();         }         countDownLatch5.await();          verify(authenticationService, times(1)).refreshTokenSync();           Timber.d("next time another 3 threads try to refresh the token at the same time");         final CountDownLatch countDownLatch3 = new CountDownLatch(3);         for (int i = 0; i < 3; i++) {             new Thread(new Runnable() {                 @Override                 public void run() {                     try {                         interceptor.intercept(chain);                         countDownLatch3.countDown();                     } catch (IOException e) {                         throw new RuntimeException(e);                     }                 }             }).start();         }         countDownLatch3.await();          verify(authenticationService, times(2)).refreshTokenSync();           Timber.d("1 thread tries to refresh the token");         interceptor.intercept(chain);          verify(authenticationService, times(3)).refreshTokenSync();     }      private Response createUnauthorizedResponse() throws IOException {         Response response = mock(Response.class);         when(response.code()).thenReturn(401);         return response;     }      private Request createFakeRequest() {         Request request = mock(Request.class);         Request.Builder fakeBuilder = createFakeBuilder();         when(request.newBuilder()).thenReturn(fakeBuilder);         return request;     }      private Request.Builder createFakeBuilder() {         Request.Builder mockBuilder = mock(Request.Builder.class);         when(mockBuilder.header("access_token", FRESH_ACCESS_TOKEN)).thenReturn(mockBuilder);         return mockBuilder;     }  } 
like image 176
raducoti Avatar answered Sep 29 '22 15:09

raducoti


You should not use interceptors or implement the retry logic yourself as this leads to a maze of recursive issues.

Instead implement the okhttp's Authenticator which is provided specifically to solve this problem:

okHttpClient.setAuthenticator(...); 
like image 33
Greg Ennis Avatar answered Sep 29 '22 17:09

Greg Ennis