Cognito × AmplifyでOpenID ConnectのAuthorization Code Flowを使うときのセキュリティが気になった

Digital Identity技術勉強会 #iddance Advent Calendar 2020|10日目、AWS Amplify Advent Calendar 2020|11日目のクロスポストです。

OpenID Connect準拠のIdPと連携したSign-Inを実装する必要がある場合、

- コストが安いマネージドサービスを使いたい
- リライングパーティの実装を極力減らしたい

というお気持ちになるのではないでしょうか。
バックエンドがAWSでフロントエンドはSPAの時、Cognito × Amplifyの利用が解決策となる場合があります。
その際、Authorization Code Flowで実装すべきセキュリティ対策ができているか気になったので確認してみました。

## 準備
そもそもCognito × AmplifyでOpenID Connect準拠のIdPと連携したSign-Inを実装するには何をすれば良いのでしょうか。
ここでは、OIDC IdPは準備済みの前提で、Cognito,Amplifyの設定について簡単に記載します。

### Cognitoの設定
amplify-cliなどでCognitoユーザープールを作成し、`フェデレーション>IDプロバイダー`に連携先のOIDC IdpのクライアントID・シークレット・URLなどを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94938/05b05c1b-b5a7-8820-15a0-7bd45a9e6d79.png)
アプリクライアントの設定に、コールバックURLとサインアウトURLを設定します。ここではテストのため、ローカルで起動するwebpack-dev-serverのURL( http://localhost:3000 )を指定しました。SPAはパブリッククライアントになるため、Authorization Code Flowのみを許可します。フロントエンドでアクセストークンを扱う必要がないのであればImplicit Flowのチェックは外しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94938/78fb7870-4627-ce7b-2adb-6944bfcc1d5b.png)
コールバックURLとサインアウトURLは、OIDC IdP側でも許可しておく必要があります。加えて、コールバックURLにはCognitoユーザープールドメインURLも追加する必要があります。

```
https://<your-user-pool-domain>/oauth2/idpresponse
```

### Amplifyの設定
Cognitoの準備ができたら、Amplifyを使ってSign-Inするだけです。
[公式ドキュメント](https://docs.amplify.aws/lib/auth/social/q/platform/js#setup-frontend)のReactのサンプルを参考にSign-Inボタンを設置します。

```jsx
<button onClick={() => Auth.federatedSignIn({provider:'<OIDC Provider Name>'})}>Sign In</button>
```

## Authorization Code Flowによるログイン時のセキュリティ
[OpenID ConnectのAuthorization Code Flow](http://openid-foundation-japan.github.io/openid-connect-basic-1_0.ja.html
)を参考にシーケンスのイメージを記載しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94938/1634ce58-56a4-6f43-66f9-a23b51917757.png)

Authorization Code Flowの最初のリクエストでは、セキュリティ対策上state,nonce,PKCEの指定が必要です。
参考：https://ritou.hatenablog.com/entry/2019/07/08/070000

- state:CSRF対策のランダム文字列
- nonce:リプレイアタック対策のランダム文字列
- PKCE(code_challenge): 認可コード横取り対策のランダム文字列のハッシュ値

Amplify SDKによって、CognitoへのInitiateリクエストが自動生成されます。このタイミングで上記のパラメータを指定しないと、フローの後続で検証されることもないので、ここがセキュリティ対策がされているかどうかの肝です。
実際に飛ばされたURLはこちら

```
https://<your-user-pool-domain>/oauth2/authorize?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&response_type=code&client_id=7o8ves8p5rq6q6o7i0mise4mbo&identity_provider=Auth0&scope=email%20openid%20profile%20aws.cognito.signin.user.admin&state=gnpKG6iF9Wknn9pSNNZQhmuzIAYFoXxO&code_challenge=CEloN-emb6Qb8UYHRBkOeXq1yfFo1g5S_Ak04dwMrqw&code_challenge_method=S256
```

state,PKCE(code_challenge)は指定されていました。後続で検証もされていました。

- 認可コード取得後のリダイレクトで、同じstateがパラメータに入っていることを検証していた
- IDトークンの取得リクエストに、PKCEのハッシュ化前のランダム文字列(code_verifier)を含めていた

nonceについては、最初のリクエストの時点でパラメータが指定されていませんでした。

## まとめ

- Cognito × AmplifyでOpenID Connect準拠のIdPと連携したSign-Inが簡単に画面ポチポチで準備できる。
- Amplifyを使えばアプリケーションへの組み込みもSign-In部分だけなら実質コード1行で、state,PKCEのパラメータ設定と検証をやってくれるのはリライングパーティの実装を減らせてありがたい。
- ~~なんでnonceを設定しなくていいんだろう。オプションなどで設定する方法はamplifyのライブラリのコードを見た限りなかった。今後自動で設定してくれる／オプションで設定できるようになることを期待。~~  
コメントで教えていただきましたが、nonceで対策するリプレイアタックに対しては、認可コードがワンタイムになっていることで対策可能です。Cognitoが認可コードをワンタイムで発行してくれていることを確認すれば、安心して使えそうですね。