OkHttp: How to Refresh Access Token Efficiently

When you use the token-based authentication including OAuth, there are two tokens: access token and refresh token. Whenever you need to access a protected resource, An access token should be used to approve the access right. A refresh Token is a kind of special token. It can be used to get a renewed access token because an access Token has a short lifecycle. The value of expires_in in the response shows how long the access token is valid.

{
  "token_type":"bearer",
  "access_token":"{ACCESS_TOKEN_VALUE}",
  "expires_in":3600,
  "refresh_token":"{REFRESH_TOKEN_VALUE}"
}

Typically, an access token is used in the request header. OkHttp supports a handy way to tweak your request before sending it, which is Interceptor. With that, you can add an access token information in the header easily without modifying your request call methods.

OkHttpClient client = okHttpClient.newBuilder()
  .addInterceptor(new AccessTokenInterceptor())
  .build();
public class AccessTokenInterceptor implements Interceptor {

  private final AccessTokenRepository accessTokenRepository;

  public AccessTokenInterceptor(AccessTokenRepository accessTokenRepository) {
    this.accessTokenRepository = accessTokenRepository;
  }

  @Override
  public Response intercept(Chain chain) throws IOException {
    String accessToken = accountRepository.getAccessToken();
    Request request = newRequestWithAccessToken(chain.request(), accessToken);
    return chain.proceed(request);
  }

  @NonNull
  private Request newRequestWithAccessToken(@NonNull Request request, @NonNull String accessToken) {
    return request.newBuilder()
            .header("Authorization", "Bearer " + accessToken)
            .build();
  }
}

This works while an access token is valid. Since the access token can be expired, you need to think how to refresh it. One naive approach could be to schedule a job to refresh before expires_in. It could work but there are things you need to consider.

  • Application Lifecycle: When an application is stopped, a scheduled job could be gone. You would reschedule the job with considering the last refresh time when the application is started.

  • Unnecessary Refresh: When a user doesn’t access a protected resource for a long time, you don’t need to refresh an access token. If there is a refreshing every 60 minute considering expires_in, there would be unnecessary refreshing 24 times for one day.

Interceptor Approach

While intercepting, Interceptor allows you not only to modify your request but also to send a request and get a response. When an access token is expired, there is an HTTP unauthorized error, with 401 error code, from the backend. That is the best time to refresh an access token.

Below code shows how to do it. First, you need to send a refresh access token request. Then, retry the original request with the renewed access token. There are multiple threads in OkHttp to handle requests. It is important to use synchronized to avoid additional refreshing.

public class AccessTokenInterceptor implements Interceptor {

  ...

  @Override
  public Response intercept(Chain chain) throws IOException {
    String accessToken = accessTokenRepository.getAccessToken();
    Request request = newRequestWithAccessToken(chain.request(), accessToken);
    Response response = chain.proceed(request);

    if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
      synchronized (this) {
        final String newAccessToken = accessTokenRepository.getAccessToken();
        // Access token is refreshed in another thread.
        if (!accessToken.equals(newAccessToken)) {
            return chain.proceed(newRequestWithAccessToken(request, newAccessToken));
        }

        // Need to refresh an access token
        final String updatedAccessToken = accessTokenRepository.refreshAccessToken();
        // Retry the request
        return chain.proceed(newRequestWithAccessToken(request, updatedAccessToken));
      }
    }

    return response;
  }

  ...
}

Authenticator Approach

Interceptor approach could be good enough to refresh an access token. Since the token-based authentication is getting common, OkHttp supports a better way, Authenticator. Authenticator is specially designed for refreshing an access token. There are three major benefits to use:

  • Auto Retry: If you set a request, it will retry it up to 20 times by default.
  • No Response Code Check: In the Interceptor approach, you manually check the response code to determine refreshing. Authenticator is only called when there is an HTTP unauthorized error, so you don’t need to check.
  • No Interceptor Side Effect: OkHttp can have multiple Interceptors. Each interceptor is called orderly what you set and next Interceptor is executed when chain.proceed() is called. In Interceptor approach, there are two chain.proceed(). One is for the original request and the other is for retry request. In each time, the next Interceptor is executed, so AnotherInterceptor could be called twice. Depending on what it does, this could make a side effect.

Authenticator is triggered for every HTTP unauthorized error. For some authentication type such as Basic, it is not needed to refresh the access token. So, below code checks the header information first. The return type is @Nullable. If you return null, it doesn’t do anything. Also, unlike Interceptor, it returns Request not Response.

OkHttpClient client = okHttpClient.newBuilder()
  .authenticator(new AccessTokenAuthenticator())
  .addInterceptor(new AccessTokenInterceptor())
  .build();
public class AccessTokenAuthenticator implements Authenticator {

  private final AccessTokenRepository accessTokenRepository;

  public AccessTokenAuthenticator(AccessTokenRepository accessTokenRepository) {
    this.accessTokenRepository = accessTokenRepository;
  }

  @Nullable
  @Override
  public Request authenticate(Route route, Response response) {
    final String accessToken = accountRepository.getAccessToken();
    if (!isRequestWithAccessToken(response) || accessToken == null) {
        return null;
    }
    synchronized (this) {
        final String newAccessToken = accountRepository.getAccessToken();
        // Access token is refreshed in another thread.
        if (!accessToken.equals(newAccessToken)) {
            return newRequestWithAccessToken(response.request(), newAccessToken);
        }

        // Need to refresh an access token
        final String updatedAccessToken = accountRepository.refreshAccessToken();
        return newRequestWithAccessToken(response.request(), updatedAccessToken);
    }
  }

  private boolean isRequestWithAccessToken(@NonNull Response response) {
      String header = response.request().header("Authorization");
      return header != null && header.startsWith("Bearer");
  }

  @NonNull
  private Request newRequestWithAccessToken(@NonNull Request request, @NonNull String accessToken) {
      return request.newBuilder()
              .header("Authorization", "Bearer " + accessToken)
              .build();
  }
}

Conclusion

OkHttp supports token-based authentication well. Interceptor allows you to add the access token information in the header easily. For refreshing the access token, you can use Authenticator. It is specially designed for that and triggered only when there is an HTTP unauthorized error. Moreover, it will retry automatically and doesn’t call Interceptor additionally.

點(diǎn)擊跳轉(zhuǎn)原文

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瓢捉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子办成,更是在濱河造成了極大的恐慌,老刑警劉巖搂漠,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迂卢,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡桐汤,警方通過查閱死者的電腦和手機(jī)而克,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)怔毛,“玉大人员萍,你說(shuō)我怎么就攤上這事〖鸲龋” “怎么了碎绎?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)抗果。 經(jīng)常有香客問我筋帖,道長(zhǎng),這世上最難降的妖魔是什么冤馏? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任日麸,我火速辦了婚禮,結(jié)果婚禮上逮光,老公的妹妹穿的比我還像新娘代箭。我一直安慰自己,他們只是感情好涕刚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布嗡综。 她就那樣靜靜地躺著,像睡著了一般副女。 火紅的嫁衣襯著肌膚如雪蛤高。 梳的紋絲不亂的頭發(fā)上蚣旱,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音戴陡,去河邊找鬼塞绿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛恤批,可吹牛的內(nèi)容都是我干的异吻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼喜庞,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼诀浪!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起延都,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤雷猪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后晰房,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體求摇,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年殊者,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了与境。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡猖吴,死狀恐怖摔刁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情海蔽,我是刑警寧澤共屈,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站党窜,受9級(jí)特大地震影響趁俊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜刑然,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一寺擂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泼掠,春花似錦怔软、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至腻豌,卻和暖如春家坎,著一層夾襖步出監(jiān)牢的瞬間嘱能,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工虱疏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惹骂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓做瞪,卻偏偏與公主長(zhǎng)得像对粪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子装蓬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容