SPA・Auth0の認可フローとセッション管理

# 概要
Single Page Application（SPA）でAuth0を使用する際に
- どのような仕組みでアクセストークンの発行が行われるか
- どのようにアクセストークンの再発行を実現しているか（期限切れやリロード時）
- セッション管理にどの方法を採用すればよいか

をまとめた．

以下では[Auth0 Universal Login](https://auth0.com/docs/authenticate/login/auth0-universal-login)を想定するものの，多くの部分は[OAuth2.0のAuthorization Code Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1)に共通のフローを解説している．

# ログインフロー
## ログイン画面でのログイン時
![login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631275/c080a9f9-68a7-c859-be5d-4bfb7989afb6.png)
※認可コードの送信処理は少し簡略化している．実際には[PKCE](https://qiita.com/ist-n-m/items/992c67b803ff460818ec)と呼ばれる機構を用いることで，認可コードが奪取される可能性を低減している．

### 疑問点と答え
- Q. なぜアクセストークンを直接返すのではなく一度認可コードを経由する？
- A. Auth0ログイン画面からアプリ画面へのリダイレクト時に秘密情報をクエリパラメータ（URIフラグメント）で渡す必要があり，流出しやすいため．認可コードの期限はごく短く設定されており[^1]，保存も不要なことから流出の可能性・リスクを最小限にできる．

## アクセストークンの再発行 ― Silent Authentication
Auth0のQuickstartで公開されているSPAサンプルアプリ（およびその元になっている [auth0-spa-js](https://github.com/auth0/auth0-spa-js)）がデフォルトで採用している仕組みは[Silent Authentication](https://auth0.com/docs/authenticate/login/configure-silent-authentication)と呼ばれる．以下の図に示すようにセッションIDを用いることで，たとえリロードを行うケースであってもログイン画面を表示することなくアクセストークンを再発行できる：
![silent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631275/f7eaf515-e06e-15b3-47bb-9cf8b5916e94.png)

### 疑問点と答え
- Q. なぜリロードにも対応できる？
- A. セッションIDはCookieに保存されるため．さらに，CookieはJavaScriptから読み込めない設定にできるのでXSSで漏出しない点も重要．
　
- Q. なぜ不可視のiframeのような複雑な仕組みが使われている？
- A. Authorization Code GrantがHTTPのリダイレクトに基づいているため．アプリ画面上で認可コードを発行するとリダイレクトにより画面が切り替わってしまいUXが損なわれる．そこで，見えないiframeを使って密かに（"silent"に）認可コードを発行した上でアプリ画面に受け渡すことで，UXを損ねることなく認可コードを取得できる．

## アクセストークンの再発行 ― Refresh Token Rotation
Silent AuthenticationはUXを損なわずにアクセストークンを再発行できる便利な仕組みだった．しかし，近年のブラウザはトラッキング保護のためCookieの使用を厳しく制限する~~おせっかい~~セキュアな仕組みが搭載されていることがあり，Silent Authenticationがうまく動作しないケースがある．この問題に対して[Auth0は2つの回避策を提示](https://auth0.com/docs/troubleshoot/authentication-issues/renew-tokens-when-using-safari)しており，そのうちの1つがRefresh Token Rotationである．リフレッシュトークンを使う場合はAuth0テナントで**リフレッシュトークンのローテーション頻度などを適切に設定**（重要）した上で，auth0-spa-jsのクライアント生成時に以下の設定を行う：
```js
// ReactやVueのSDKにも似た引数を設定する部分があるはず
const auth0 = await createAuth0Client({
  domain: '<your Auth0 domain>',
  client_id: '<your Auth0 client ID>',
  useRefreshTokens: true,        // リフレッシュトークンを使う
  cacheLocation: 'localstorage', // トークン等をローカルストレージに保存する
  useRefreshTokensFallback: true // (任意)リフレッシュトークンが使えないときにSilent Authenticationを試みたい場合
});
```
リフレッシュトークンをlocalStorageに保存するため，持続型XSSには脆弱だが反射型XSSに対するリスクを低減できる[^2]．
![refresh_token.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631275/b55ff301-898f-8228-3b4f-e584b8de9277.png)


### 疑問点と答え
- Q. なぜアクセストークンとリフレッシュトークンの2つがある？
- A. アクセストークンの有効期限を短くしつつUXを保つため．アクセストークンは頻繁にアプリAPIサーバに送信されるので流出が懸念される．そこで，アクセストークンの期限を短くしつつ，期限が切れたらリフレッシュトークンを用いて新しいアクセストークンを発行することで，ログイン画面を何度も表示することなくセキュリティを保てる．ここで以下の事実が重要：
  - リフレッシュトークンは（アクセストークンほど）頻繁に送信する必要がない
  - リフレッシュトークンはAuth0にだけ送信し，アプリAPIサーバには送信しない
　
- Q. リフレッシュトークンが流出したら何が起きる？
- A. 攻撃者がリフレッシュトークンを使うとアクセストークンが手に入るため不正にAPIを呼び出せる．ただし，同じリフレッシュトークンが2回使用された場合には新しいリフレッシュトークンを無効化する仕組みが組み込まれており，不正な呼び出しはごく短時間だけに限定されるようになっている[^3]．
　
- Q. クライアントでアクセストークンの期限を確認したあとAPI呼び出しをするまでに期限が切れることがあるのでは？
- A. この事象は起きうる．Auth0のクライアントSDKでは期限切れまで60秒を切っていてたらリフレッシュを行うようにすることで[^4]，この問題が実質起きないようになっている．

# 結局どれを選べばいいのか ― SPAにおける選択チャート
以上のように，セッションを保持する上では
- CookieかlocalStorageにセッション情報を保存しないといけない
- XSS対策の観点ではCookieが望ましい
- Cookieはトラッキング保護で使えないことがある

という条件のもと，セキュリティとユーザビリティを折衷することになる．これらの観点を以下のチャートにまとめた：

1. リロードしたときに毎回ログイン画面に戻っていい → YESなら[パターンA](#パターンa-リロードしたときにログイン画面に戻っていい)へ
1. カスタムドメインを設定できる → YESなら[パターンB](#パターンb-カスタムドメインを設定できる)へ
1. セキュリティをわずかに犠牲にしてもトラッキング保護が厳しいブラウザをサポートしたい → YESなら[パターンC](#パターンc-セキュリティをわずかに犠牲にしてもトラッキング保護が厳しいブラウザをサポートしたい)へ
1. 以上どれも当てはまらない→[パターンD](#パターンd-デフォルト挙動)へ 

## パターンA. リロードしたときにログイン画面に戻っていい
| テナント設定 | クライアントSDK設定 |
|--|--|
|Refresh Token Rotationを有効化|`useRefreshTokens=true`|

この方式ではCookieすら使わずアクセストークン・リフレッシュトークンをすべてメモリ上に保存する[^5]．

## パターンB. カスタムドメインを設定できる
| テナント設定 | クライアントSDK設定 |
|--|--|
|カスタムドメインをアプリURLに合わせて設定|特になし|

この方式ではカスタムドメインを設定することでSilent Authenticaitonで問題となるCookieの制約を回避する．

## パターンC. セキュリティをわずかに犠牲にしてもトラッキング保護が厳しいブラウザをサポートしたい
| テナント設定 | クライアントSDK設定 |
|--|--|
|Refresh Token Rotationを適切に設定|`useRefreshTokens=true`<br>`cacheLocation='localstorage'`|

この方式はRefresh Token Rotationを採用することでlocalStorageにリフレッシュトークンを保存するリスクを低減する．

## パターンD. デフォルト挙動
| テナント設定 | クライアントSDK設定 |
|--|--|
|特になし|特になし|

Silent Authenticationを使うAuth0のデフォルト挙動．

# 余談：Refresh Token RotationがOFFの場合ブラウザからのリフレッシュトークン発行要求は無視される
ドキュメント上記載が見つけられなかったのだが，Auth0ではRefresh Token RotationがOFFの場合`useRefreshTokens=true`を指定しても**リフレッシュトークンは発行されない**という仕様になっている[^6]．これは前節までに述べているリフレッシュトークン流出の対策と思われるが，このときtokenエンドポイントへのリクエスト自体はエラーにはならず，アクセストークンやIDトークンだけが発行される．この事象が発生している場合，テナントのログには以下のWarningが出るが，レスポンスからは原因がわかりにくいので注意：
>The scope 'offline_access' was requested, but no 'refresh_token' was issued because the authorization code exchange originated from a browser

[^1]: [RFC6749では10分以内が推奨されている](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2)
[^2]: [Auth0 Blog: You Can Store Refresh Token In Local Storage](https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/#You-Can-Store-Refresh-Token-In-Local-Storage)
[^3]: [Automatic Reuse Detection](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation#automatic-reuse-detection)と呼ばれる．リフレッシュトークンの無効化は，本物のユーザと攻撃者のどちらが先にリフレッシュを試みたかによらず行われる．本物のユーザが先にリフレッシュした場合は，攻撃者はリフレッシュに失敗するので不正呼び出しはできない．攻撃者が先にリフレッシュした場合は，その後本物のユーザがリフレッシュを試みた時点で無効化が行われるため，少なくともアクセストークンの寿命1回分の間だけは不正呼び出しの可能性がある．
[^4]: https://github.com/auth0/auth0-spa-js/blob/8653181d23ecea17729da203253d4fa4d17d8ef6/src/Auth0Client.ts#L614
[^5]: 別な方法として，Silent Authenticationを採用した上でCookieをブラウザ終了時に消す方法（Non-Persistent Mode）がある．この場合はCookieにセッションIDを保存するためXSSに対する防御が強力だが，トラッキング保護の問題が残る．
[^6]: 言及されていたコミュニティ投稿：https://community.auth0.com/t/spa-application-error-no-refresh-token-was-issued-authorization-code-exchange-originated-from-a-browser/117730