😬

[iOS]AccessTokenとRefreshTokenの寿命が同じになっていた話

2025/01/15に公開

概要

MobileApp開発で、AccessTokenRefreshTokenの寿命が同じ値になっていたことが原因のバグに遭遇した。
AccessTokenRefreshTokenとは何か、なぜ同じ寿命を設定すると問題が発生するのかについて書く。
教訓:
RefreshTokenの寿命は、AccessTokenの寿命より長く設定されているべきである。

背景

ある日、Backendチームから、Appからのリクエストで401エラー(Not authorized)が多発していると報告があった。
401エラーは、たとえば失効したAccessTokenを使用してAPIを呼んだときに発生する。
AccessTokenに寿命があるため、401エラーが発生することは想定内だが、その頻度が異常に高いということであった。

App側では、401エラーが返ってくるとRefreshTokenを使用してAccessTokenを更新し、再度リクエストを送るようになっていた。
AccessTokenの更新ロジックは長い間変更してない&これまでは問題がなかったので、正しく動いていると考えられた。
何が起きているか調査を始めた。🤔

発生していたこと

エラーログなどを分析をすると、401エラーが返ってきたときに、想定通りAccessTokenを更新する処理が呼ばれていた。
しかし、AccessTokenの更新時に、"RefreshToken expired" というようなエラーが発生していた。
どうやら、RefreshTokenが失効していたため、AccessTokenの更新がうまくいっていなかったらしい。

AccessTokenとRefreshToken

AccessTokenとは

クライアント(e.g. Mobile App)がリソース(e.g. サーバが保持するデータ)にアクセスするために必要な情報を保持するトークン。
クライアントはAccessTokenをサーバに送信し(下図①)、サーバはそのトークンを検証して、リソースへのアクセスを許可するかどうかを決定する(下図②)。
たとえば、ユーザーがログインしたときに、サーバからAccessTokenを受け取り、そのトークンを使ってログインユーザーのみが閲覧できる情報を取得する。(自分のメールアドレスや、購入履歴など)

AccessTokenは、制限されたリソースへのアクセスを許可するために使用されるため、それが漏洩してしまうと、他人によるなりすましが成立する可能性がある。
AccessTokenは、サーバ側へのリクエストを行うたびに送信されるため、漏洩の可能性が(RefreshTokenと比べて)比較的高い。
そのリスクを軽減するため、AccessTokenには短めの寿命が設定される。
盗難されても、寿命が短いならがすぐに使えなくなるからである。
寿命が短いほど安全だが、更新の頻度が増えるので、安全とUXのトレードオフがある。

RefreshTokenとは

新しいAccessTokenを発行するために必要な情報を保持するトークン。
一般的に、AccessTokenの寿命が切れた後に、新しいAccessTokenを取得するために使用する。

たとえば、AccessTokenが失効すると、RefreshTokenを認証サーバに送信し(下図①)、認証サーバはそのトークンを検証して新しいAccessTokenを発行する(下図②)。
クライアントは新しいAccessTokenを受け取り(下図③)、それを使ってリソースにアクセスする(下図④)。

RefreshTokenにも寿命が設定されるが、AccessTokenよりも長く設定される。
AccessTokenの更新のときにのみ送信されるため、漏洩の可能性が(AccessTokenと比べて)比較的低い。
RefreshTokenはDeviceに保存されることが多い。これが盗難されると、AccessTokenを取得されるリスクがあるため、セキュアな保存が必要である。

比較

項目 AccessToken RefreshToken
用途 リソースアクセス AccessToken の更新
寿命 短い 長い
使用頻度 高い 低い
セキュリティリスク 漏洩リスクが高い 漏洩リスクが低い(慎重な管理が必要)
保管場所 メモリやセキュアなストレージ セキュアなストレージ

今回の問題の原因

App側では、AccessTokenRefreshTokenをBackendに発行してもらい、それを使用してAPIを呼んでいる。
AccessTokenRefreshTokenを発行するときに、それらの寿命を設定していたのだが、あるときからそれらを同じ値にしてしまっていた
本来であれば、AccessTokenの寿命が切れたときは、RefreshTokenを使用してAccessTokenを更新できる。
しかし、RefreshTokenにも同じ寿命が設定されていたため、RefreshTokenAccessTokenと同時に寿命が切れてしまう。
そのため、Accesstokenの更新時にRefreshTokenも失効していたため、更新に失敗していた。
その後も失効したAccessTokenを使用してAPIを呼び続けていたため、401エラーが多発していた。

解決策

RefreshTokenの寿命を、AccessTokenの寿命より長く設定する。
これによって、AccessTokenの寿命が切れたときに、RefreshTokenを使用してAccessTokenを更新できるようになる。

再発防止に向けて

  • Backendチームと協力して、401エラーの頻度が異常なときに報告してもらう。
  • QAにテストしてもらう。AccessTokenRefreshTokenの寿命が数日だと検証が難しいので、QA用にトークンの寿命を短くしたビルドを提供する。
  • AccessTokenRefreshTokenの違いについて他のメンバーにも共有する。(本記事)

教訓

RefreshTokenの寿命は、AccessTokenの寿命より長く設定されているべきである。

Ref

Discussion