Auth0 の Silent AuthenticationとRefresh Token Rotation、完全に理解した
Auth0 の Silent Authentication (サイレント認証)と Refresh Token Rotation (リフレッシュトークンローテーション)を完全に理解したい気持ちが急に高まってきたので書きます。
全体の構成として
- React SPA with Auth0 での認可フローについて
- Silent Authentication (サイレント認証)について
- Refresh Token Rotation について
- まとめ
みたいな流れで書きつつ、Silent Authentication や Refresh Token Rotation は何を解決しようとしているのか、それぞれのリフレッシュ方法でどのような挙動になるのか、などについて理解を深めていきたいと思います。
また、React SPA with Auth0 の認可フロー部分のイメージを沸かせるために以下のサンプルリポジトリを用意しています:
こちらは
- frontend
- React の SPA でログイン、アクセストークンを取得する
- backend
- Rails ベースの GraphQL API は SPA からアクセストークン (JWT)を受け取って、トークンを検証する
- 検証が成功したら認可が必要な情報を返却する
- Rails ベースの GraphQL API は SPA からアクセストークン (JWT)を受け取って、トークンを検証する
- auth0
- Auth0 も一応コードからデプロイして環境構築可能
- めっちゃ個人名の入ったアプリ名になってしまっているので名前変えてください
- 一部、設定ファイルだけでは更新できない設定もあるので注意 (デプロイ用のアプリ作成など)
- Auth0 も一応コードからデプロイして環境構築可能
のような構成になっていて、アクセストークン取得まわりの動作検証に使ったものなのでよかったら見てみてください。ちなみに GraphQL とか使っちゃっていますが、今回の文脈において GraphQL の知識は特に必要ありません。
React SPA with Auth0 での認可フロー
Auth0 の React 用 SDK: auth0-reactを利用した場合、認可フローはこちらに Under the hood, it implements Universal Login and the Authorization Code Grant Flow with PKCE.
と書かれているように、Authorization Code Flow with Proof Key for Code Exchange (PKCE)になる。より詳細な説明はこちらに書かれている。
PKCE は雑にいうと、「OAuth2 の Authorization Code Flow で認可コード奪われたらアクセストークンも奪われちゃうからツラいですよね、認可コード送信元も検証して、せめてアクセストークンは取られんようにしましょうや」っていう仕組み。こちらの記事の解説がめちゃめちゃわかりやすかったのでオススメ。
auth0-react
を使う場合、Authorization Code Flow with Proof Key for Code Exchange (PKCE)な実装は SDK 側でサポートしてくれているので自前で code_challenge
などをハンドリングするような必要はない。
ちなみにauth0-react
は元々あったauth0-spa-jsを React の Hooks ベースの実装でいい感じに抽象化しただけのものなので、実装詳細を知りたい場合はauth0-spa-js
の方のコードを読んだほうがよい (読まざるを得ない)。
ログイン -> アクセストークン取得 -> ログアウトまでのライフサイクル
会員登録済みで Auth0 上にすでにユーザが存在する前提で、ログインからアクセストークン取得、ログアウトまでの流れはざっくり以下のようになると思う。
-
auth0-react
のloginWithRedirectを利用してログイン処理を呼び出す - Auth0 の Application (SPA)で callback URL として許可している URL に
?code=xxx
のクエリパラメータ付きでリダイレクトされる-
code
などが URL に含まれててツラいが、auth0-react
の onRedirectCallbackを指定しておくと勝手にその辺のツラいクエリパラメータが除外された URL に勝手にリダイレクトしてくれる
-
- 上記の
code
を使って{auth0_domain}/oauth/token
にリクエストを投げてaccess_token
を取得- アクセストークン以外にも
scope
など色々情報は返ってくるはずだけど、今回は割愛 -
auth0-react
のgetAccessTokenSilentlyというメソッドがその辺やってくれている
- アクセストークン以外にも
- トークンストレージはデフォルトではインメモリ
- 画面をリロードしたらアクセストークンは再取得する必要がある
- localStorage を利用することも可能 (後述するけど、リスクを理解せずノリで使うのは危険)
-
auth0-react
のlogoutを呼ぶとログアウトし、Auth0 ドメインとのセッションも切れる
Silent Authentication (サイレント認証)
上述したが、アクセストークンはデフォルトではインメモリにキャッシュされるので画面リロードなどで都度、再取得が必要になる。「Universal login -> ログイン成功 -> ?code=xxx
付きで SPA にコールバック」の流れでは特に問題なかったが、それ以降、Auth0 ドメインとのセッションが生きている状態でアクセストークンの再取得はどうするのか、またログインからやり直しなのか...?
Auth0 はアクセストークンをリフレッシュする手段としてSilent Authentication (サイレント認証)という仕組みをサポートしている。2021 年 6 月時点ではデフォルトで Silent Authentication がデフォルトのアクセストークンリフレッシュ手段になっている。リフレッシュトークンは auth0-react
の実装で Provider にuseRefreshTokens=true
を渡すと利用できるようになる (その他、Auth0 の設定側で offline_access
スコープを API で許可する設定なども必要)。
これは雑に説明すると、画面遷移などを介さずひっそりとセッションを確認してアクセストークンを再取得する仕組みで、Chrome のネットワークタブなどをよく見てみると{auth0_domain}/authorize?xx=xx
へのリクエストで prompt=none
とresponse_mode=web_message
が含まれているのが重要なポイントである。
prompt=none
をつけることで画面表示が行われない状態でセッション確認ができる。そしてresponse_mode=web_message
によってユーザからは見えない iframe
を作って、「そいつから {auth0_domain}/authorize
にリクエスト -> code
の含まれたレスポンスを受け取る -> そいつを Web Messaging API 経由で親に渡してアクセストークン再取得」みたいな動きを実現している。
詳しくはこちらの記事がわかりやすく解説してくださっている。
こいつのメリットは localStorage を使わないで、かつ毎回ログインを要求されるみたいなクソな挙動を避けてアクセストークンを再取得できることだと思う。SPA はブラウザが前提になるのでネイティブと違ってセキュアなストレージがない。こちらの記事にあるように localStorage は悪意のある npm ライブラリ経由でシュッとトークンが抜かれるリスクがあるなど、セキュアではない。アクセストークンのようなデリケートな情報を長期間そこに入れておくのは危険なので、そういったリスクを避け、かつユーザ体験をそこまで損なわないこの仕組みは非常によい。
ただこいつも完璧ではなく、こちらに書かれているように、昨今の Safari の ITP のような、3rd party cookie がブロックされがちなご時世では Silent Authentication がうまく動作しないケースがある。
実際、Safari や Chrome で 3rd party Cookie をブロックする設定のまま Silent Authentication ベースのアクセストークンの再取得を試みると期待通り動作せず、以下のような感じになる。
- SPA (iframe) <-> Auth0 ドメイン間でのリクエストのやり取りの際に 3rd party cookie に当たる情報が取得できない
- セッション確認失敗
-
login_required
の Error Response が返るため、code
が取得できない - アクセストークンも取得できない
これを回避するには Auth0 のカスタムドメイン機能を使って SPA をapp.example.com
、ログイン URL を login.example.com
みたいに設定して回避するか、後述の Refresh Token Rotation がサポートされたリフレッシュトークンを使う、あたりが選択肢になってくる。
Refresh Token Rotation (リフレッシュトークンローテーション)
上述のような Silent Authentication の弱点をカバーするための手段として生まれたのが Refresh Token Rotation (という理解で合ってる?)。詳しい説明はこちらに書かれている。
これまでリフレッシュトークンに有効期限がなかったので、それを localStorage に入れるのはリスキーだったが、Refresh Token Rotation を使うとリフレッシュトークンを使ってアクセストークン取得する際に新しくリフレッシュトークンも再発行され、以前のものは無効化されるのでよりセキュアにリフレッシュトークンが使えるようになった。
もちろんリスクが無くなるわけではないが、有効期間をそれなりに短めに設定して localStorage にトークンをキャッシュするアプローチも悪くはないと思う。
Refresh Token Rotation を有効にして localStorage をキャッシュに利用した場合のフローは以下のようになると思う。実際に色々設定をいじったり、 auth0-spa-js
のコードを読みながら挙動を確認したんですが、間違ってたらご指摘ください。
アクセストークンの有効期限が切れる -> リフレッシュトークンを使って再取得の流れ
まずアクセストークン取得からそいつの有効期限が切れる -> リフレッシュトークンを使って再取得するまでの流れは大体こんな感じかと思う。ちなみにアクセストークンの有効期限はデフォルトで 1 日、Auth0 API の Token Expiration の設定に依存する。
- 最初のログインで Auth0 のセッションができる
- ここでアクセストークンとリフレッシュトークンを取得
- localStorage にこれらの情報が保存される
- localStorage の有効期限はアクセストークンの有効期限と同じになる
- アクセストークンの有効期限が切れた場合
- localStorage のキャッシュもクリアされる
- リフレッシュトークンを使ってアクセストークンを再取得
- このタイミングでリフレッシュトークンも更新されるので、以前のリフレッシュトークンは無効になる
- localStorage 上の値も更新される
アクセストークンの有効期限が切れる -> リフレッシュトークンも有効期限切れ / 無効だった場合の再取得の流れ
次にアクセストークンが無効になって再取得しようにもリフレッシュトークンも期限切れ、もしくは無効だった場合は設定によって挙動が変わる。ちなみにリフレッシュトークンの有効期間は Auth0 の SPA の Refresh Token Expiration にある Absolute Lifetime の設定に依存する。
- リフレッシュトークンが invalid となって 403 が返る
- セッション有効期間 > リフレッシュトークン有効期間な設定の場合
- リフレッシュトークンが使えなかった場合の Fallback として、Silent Authentication が呼ばれる
- セッション有効期間内の場合
- 3rd party cookie が問題なく使える場合、ここで iframe 経由で
code
を取得 -> アクセストークン、リフレッシュトークンも取得できる - 3rd party cookie が使えない場合、Silent Authentication が失敗、内部的にlogout({ localOnly: true })が呼ばれる
- ローカルのキャッシュや Cookie の
auth0.is.authenticated
の値がクリアされる - ここでは Auth0 セッションは生きている想定なので振り出しに戻る (おそらく
{auth0_domain}/authorize
にresponse_mode=query
で再リクエスト ->code
、アクセストークン、リフレッシュトークン再取得になるかな、あまり自信ない...)
- ローカルのキャッシュや Cookie の
- 3rd party cookie が問題なく使える場合、ここで iframe 経由で
- セッション有効期間を過ぎている場合、再ログインを要求される
- セッション有効期間内の場合
- リフレッシュトークンが使えなかった場合の Fallback として、Silent Authentication が呼ばれる
- セッション有効期間 < リフレッシュトークン有効期間な設定の場合
- リフレッシュトークンの有効期間が実質セッションの有効期間みたいになる
- セッション有効期間 > リフレッシュトークン有効期間な設定の場合
まとめ
- Silent Authentication はブラウザのあまりセキュアではないストレージに頼らないかつ、UX を損なわないアクセストークンのリフレッシュを実現できるが、3rd party Cookie の扱いに厳しくなってきた昨今、いつ動かなくなるかわからないというリスクがある
- 代替案として Refresh Token Roation のサポートされたリフレッシュトークンを利用する
- もしくは Auth0 のカスタムドメインを設定して、3rd party cookie を扱っているとみなされないよう設定することで解決することもできるかもしれない
- Refresh Token Rotation はリフレッシュトークンをローテーションさせることで以前よりも気軽にトークン情報をブラウザのストレージに入れられるようになったし、3rd party cookie、いつ使えなくなるんだろうという恐怖から開放される(と信じたい)
参照
- https://github.com/danimal141/auth0-react-playground
- https://github.com/auth0/auth0-react
- https://auth0.com/docs/libraries/auth0-react
- https://auth0.com/docs/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce#how-it-works
- https://dev.classmethod.jp/articles/oauth-2-0-pkce-by-auth0/
- https://github.com/auth0/auth0-spa-js
- https://auth0.com/docs/sessions/cookies/authentication-api-cookies
- https://qiita.com/smesh/items/2673287c16524da3886d
- https://auth0.com/docs/authorization/configure-silent-authentication
- https://qiita.com/ksakiyama134/items/bb5a97bf7762b4a87e0d
- https://techracho.bpsinc.jp/hachi8833/2019_10_09/80851
- https://auth0.com/docs/authorization/renew-tokens-when-using-safari
- https://auth0.com/docs/tokens/refresh-tokens/refresh-token-rotation
Discussion