Auth0のリフレッシュトークンのローテーションを試してみる

Auth0でリフレッシュトークンのローテーションができるようになっていたので試してみました。

Auth0のBlog : [Securing Single Page Applications with Refresh Token Rotation](https://auth0.com/blog/securing-single-page-applications-with-refresh-token-rotation/)

## Auth0のリフレッシュトークンのローテーション
Auth0のリフレッシュトークンには有効期限がなかったので、ずっと同じリフレッシュトークンを使って新しいアクセストークンを取得することができました。
しかし、今回追加されたAuth0のリフレッシュトークンのローテーション機能を有効にすると、リフレッシュトークンを使って新しいアクセストークンを取得する際に、一緒に新しいリフレッシュトークンも返却されるようになります。また、同時に一度使用したリフレッシュトークンは無効化されるので、以降は都度新しいリフレッシュトークンを使ってトークンのリフレッシュをしていくことになります。

Auth0では上記のような仕様になっていますが、こういったなんらかの方法でリフレッシュトークンをローテーションする機能については、元々、[OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-14)で推奨事項として挙げられています。

追記：リフレッシュトークンのローテーションはSecurity BCPが初出のような書き方をしてしまいましたが、実際は[RFC 6819 OAuth 2.0 Threat Model and Security Considerations](https://openid-foundation-japan.github.io/rfc6819.ja.html)の時点で記載がありました。「5.2.2.3.  Refresh Towken Rotation」に記載されています。

## コマンドラインで試す
適当なアプリを立ち上げて取得したリフレッシュトークンを使ってコマンドラインベースで動作確認をしてみました。

### Auth0に登録したアプリの設定
Auth0のDashboard > Applications > 動作確認用に立ち上げたアプリ を選択し、アプリ設定を開くと下のほうにリフレッシュトークンに関する設定項目があります。今回は以下のような設定で試してみました。

<img width="743" alt="スクリーンショット 2020-04-16 16.46.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490013/4da5ad03-ca28-50e3-971f-027eb317c45f.png">


- Refresh Token Behavior
    - Non-RotatingとRotaingの二択です。デフォルトはNon-RotatingなのでRotatingに変更しています。
- Refresh Token Lifetime (Absolute)
    - リフレッシュトークンの有効期限を秒単位で設定します。
    - 手元でさくっと動作確認するために有効期限をかなり短めに設定しています。
- Refresh Token Reuse Interval
    - 一度利用したリフレッシュトークンが無効化されるまでの猶予期間を秒単位で設定します。
    - この期間内であれば使用済みのリフレッシュトークンを再度使用することができます。
    - リフレッシュトークンのローテーションが同時実行された場合なんかに対応するために設定します。
    - 動作確認をやりやすくするために少し長めに設定しています。

### リフレッシュトークンの使用
リフレッシュトークンを使って新しいトークンを取得します。{}の中身は適宜読み替えてください。

```http:Request
POST https://{テナントのドメイン}/oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token={refresh_token}
```

```json:Response
{
  "access_token": "Zy9...xTfx",
  "refresh_token": "v1.MlMD...saow",
  "id_token": "eyJ...G6g",
  "scope": "openid profile email offline_access",
  "expires_in": 86400,
  "token_type": "Bearer"
}
```

リフレッシュトークンも返ってきました。
`Refresh Token Reuse Interval`の設定値の範囲内でもう一度同じリフレッシュトークンを使った場合も同じように新しいトークンを受け取ることができます。

逆に、以下の場合はリフレッシュトークンが無効化されているため、403のエラーが返ってきます。

- `Refresh Token Reuse Interval`の設定値を超えてから再度同じリフレッシュトークンを使用した場合
- `Refresh Token Lifetime`で設定した有効期限を超過したリフレッシュトークンを使用した場合

```json:Response
{
  "error": "invalid_grant",
  "error_description": "Unknown or invalid refresh token."
}
```

また、無効化済みのリフレッシュトークンを使うと、その無効化済みのリフレッシュトークン以降に発行された全てのリフレッシュトークンも無効化されます。そうなるとアプリを利用するユーザーはログインからやり直す必要があります。

## リフレッシュトークンのローテーションをサポートするAuth0のSDK

リフレッシュトークンのローテーションの導入に合わせてAuth0のSDKも更新されました。
2020/04時点では以下の3つのSDKがリフレッシュトークンのローテーションをサポートしています。

- [Auth0 SPA SDK（1.7.0）](https://auth0.com/docs/libraries/auth0-spa-js)
- [Auth0 Swift（iOS）SDK（1.23.0）](https://auth0.com/docs/libraries/auth0-swift)
- [Auth0 Android SDK（1.23.0）](https://auth0.com/docs/libraries/auth0-android)

## Auth0のサンプルアプリで試す
せっかくなのでAuth0が提供するサンプルアプリを使っても試してみました。

### 使用するサンプル
Auth0 SPA SDKを利用しているVueのSPAのサンプルのうち、BackendのAPIも備えている`02-Calling-an-API`を使用します。

[Auth0 Vue.js Samples](https://github.com/auth0-samples/auth0-vue-samples)

Backend APIがあるほうを選んだのは、有効期限を自由に設定できるBackend API向けのJWT形式のアクセストークンを扱うほうが動作確認をする上ではやりやすいからです。（Auth0のデフォルトのアクセストークン（ランダム文字列）は有効期限が24時間固定）

あとはお作法に則ってAuth0のDashBoardからApplicationとAPIを登録・設定し、アプリを動く状態にします。

### SDKのバージョンアップ
2020/04現在、サンプルアプリで使用されているAuth0 SPA SDKのバージョンは1.2.3なので、1.7.0にバージョンアップしておきます。

### リフレッシュトークンの使用を有効化する

元々、Auth0 SPA SDKはリフレッシュトークンの使用を想定しておらず、アクセストークンのリフレッシュは`prompt=none`とiframeで頑張ってましたが（[Silent Authentication](https://auth0.com/docs/api-auth/tutorials/silent-authentication)）、今回リフレッシュトークンのローテーションの導入に合わせてリフレッシュトークンのサポートが追加されています。

追記：
この記事の冒頭に記載したAuth0のBlogによると、Auth0ではSPAの場合、PKCEを使ってアクセストークンを取得。その後のトークンリフレッシュは非表示のiframeを使ってAuth0のセッションを確認し認証状態が維持されていればアクセストークンを再取得する。という動きになっていました。しかし最近のブラウザはサードパーティcookieをブロックするようになってきています。代替案としてリフレッシュトークンを使おうにも、ブラウザには特定のアプリしかアクセスできないような永続的なストレージがないため、リフレッシュトークンを長時間保存するのには適していません。そのため、セキュリティ面でのリスクの緩和策としてリフレッシュトークンのローテーションを使用するようになったそうです。

---

Auth0Clientのインスタンス作成時に`useRefreshTokens`オプションに`true`を設定することでリフレッシュトークンを使用できるようになります

```javascript:src/auth/authWrapper.js
    async created() {
      this.auth0Client = await createAuth0Client({
        domain: options.domain,
        client_id: options.clientId,
        audience: options.audience,
        redirect_uri,
        useRefreshTokens: true, // オプションを追加
      });

      try {
        if (
          window.location.search.includes("code=") &&
          window.location.search.includes("state=")
        ) {
          const { appState } = await this.auth0Client.handleRedirectCallback();
          onRedirectCallback(appState);
        }
      } catch (e) {
        this.error = e;
      } finally {
        this.isAuthenticated = await this.auth0Client.isAuthenticated();
        this.user = await this.auth0Client.getUser();
        this.loading = false;
      }
    }
  });
```

リフレッシュトークンを使う。ということは`scope`パラメータに`offline_access`を設定する必要があるかと思いましたが、どうやら`useRefreshTokens: true`を設定することで、SDK側が`scope`に`offline_access`をつけて認証リクエストを投げてくれるみたいなので、`useRefreshTokens: true`だけ設定しておけばいいみたいです。

この設定だけでリフレッシュトークンのローテーションまでサポートしてくれます。

### 動作確認

アプリを起動してログインします。
サンプルアプリのAPIをCallする画面を開き、Call APIボタンを押して結果が表示されればひとまず成功です。

<img width="850" alt="スクリーンショット 2020-04-16 22.16.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490013/e2b79c25-63d5-98a3-c20f-db599c8824c3.png">


画面上、変化がないのでわかりにくいですが、API設定で事前に設定したアクセストークンの有効期限を過ぎたあとでも再度ボタンを押せば結果が表示されます。SDKがアクセストークンの有効期限が切れている場合はリフレッシュトークンのローテーションまで考慮して新しいアクセストークンを取得しているからです。そのため、アクセストークンの有効期限が切れるたびにボタンを押してもリフレッシュトークンの有効期限が切れていない限りはリクエストはうまくいきます。

アプリの見た目からはこういった振る舞いは全然読み取れませんが、Auth0のログをみるとリフレッシュトークンが使われていることぐらいは確認できます。

<img width="545" alt="スクリーンショット 2020-04-16 22.12.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490013/4450c9d2-e4a2-2b0d-8afc-a38a7e09c70f.png">


## まとめ
Auth0にリフレッシュトークンのローテーション機能が追加されたので簡単にですが紹介してみました。Auth0の機能は日々アップデートされていってるので今後も楽しみです。

個人的には[OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-14)に書いてある内容がもっと取り込まれるといいな。と思っています。