[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