[iOS]AccessTokenとRefreshTokenの寿命が同じになっていた話
概要
MobileApp開発で、AccessTokenとRefreshTokenの寿命が同じ値になっていたことが原因のバグに遭遇した。
AccessTokenとRefreshTokenとは何か、なぜ同じ寿命を設定すると問題が発生するのかについて書く。
教訓:
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側では、AccessTokenとRefreshTokenをBackendに発行してもらい、それを使用してAPIを呼んでいる。
AccessTokenとRefreshTokenを発行するときに、それらの寿命を設定していたのだが、あるときからそれらを同じ値にしてしまっていた。
本来であれば、AccessTokenの寿命が切れたときは、RefreshTokenを使用してAccessTokenを更新できる。
しかし、RefreshTokenにも同じ寿命が設定されていたため、RefreshTokenもAccessTokenと同時に寿命が切れてしまう。
そのため、Accesstokenの更新時にRefreshTokenも失効していたため、更新に失敗していた。
その後も失効したAccessTokenを使用してAPIを呼び続けていたため、401エラーが多発していた。
解決策
RefreshTokenの寿命を、AccessTokenの寿命より長く設定する。
これによって、AccessTokenの寿命が切れたときに、RefreshTokenを使用してAccessTokenを更新できるようになる。
再発防止に向けて
- Backendチームと協力して、401エラーの頻度が異常なときに報告してもらう。
- QAにテストしてもらう。
AccessTokenやRefreshTokenの寿命が数日だと検証が難しいので、QA用にトークンの寿命を短くしたビルドを提供する。 -
AccessTokenとRefreshTokenの違いについて他のメンバーにも共有する。(本記事)
教訓
RefreshTokenの寿命は、AccessTokenの寿命より長く設定されているべきである。
Discussion