マネージドログインのAuthorization Code Flow with PKCEを覗いてみた。

## はじめに

下記リンクの通り、CognitoはOIDCの認証フローの中で認可コードを発行する処理にPKCEが利用できます。

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/using-pkce-in-authorization-code.html

ちょうど、情報処理安全支援確保士試験の勉強しているので、実際の通信を覗いてみたいと思います。

### PKCE(ピクシー)とは？

SPAのようなクライアントサイドで動作するWEBやアプリは認証コードの横取りを防ぐ必要があり、この仕組みがPKCEになります。

詳細は以下サイトをご確認ください。

https://e-words.jp/w/PKCE.html

## 通信を覗いてみる。

### 認証開始時

マネージドログイン画面を表示したタイミングで処理開始かな？と思ってたのですが、実際はID（今回はメールアドレス）を入れた時に認証が開始されます。

![スクリーンショット 2025-04-17 3.55.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/6fa592bb-1c91-4471-bf54-80d844ef8f2d.png)

クエリパラメタは以下の通りです。

```
login?client_id=ar4sjg7u1g1t16cah2rjfkih3&code_challenge=V11qZ0ganE__op3krG3POUEYb5AV_-KiK_vRTordda4&code_challenge_method=S256&identity_provider=COGNITO&lang=ja&redirect_uri=https%3A%2F%2Fstaging.d3prbb8vtbfpjg.amplifyapp.com%2F&response_type=code&scope=email+openid+aws.cognito.signin.user.admin+profile&state=zARVByIx0HRLOde7n7I9LlaTAGyIfIcH&_data=routes%2Flogin
```

一部抜粋してまとめてみます。

| パラメータ名 | 値 | 説明 |
|------------|------|------|
| code_challenge | V11qZ0ganE__op3krG3POUEYb5AV_-KiK_vRTordda4 | PKCE認証用のコードチャレンジ |
| code_challenge_method | S256 | コードチャレンジに使用されるハッシュメソッド (SHA-256) |
| redirect_uri | https://staging.xxxxxxxxxx.amplifyapp.com/ | 認証後のリダイレクト先URL |
| response_type | code | 認証レスポンスタイプ |
| scope | email openid aws.cognito.signin.user.admin profile | 要求されるアクセス権限の範囲 |

`code_challenge`は、毎回生成されるランダムな値`code_verify`を`code_challenge_method`のアルゴルムでハッシュ化した値です。

今時点でCognitoが分かるのは、何かしらの値をS256でハッシュ化した値が、`V11qZ0ganE__op3krG3POUEYb5AV_-KiK_vRTordda4`であることです。

つまり、毎回生成されるランダムな値`code_verifier`を知っているクライアントが認証を開始したクライアントである、ということになります。

### パスワード入力後

次にパスワードを入力すると、`verifyPassword`が3回呼ばれていました。

複数呼ばれることはReact等の仕様の模様です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/e28b75c9-eccc-4615-b151-ae5edee024e9.png)

同様にクエリパラメタを確認します。

```
verifyPassword?client_id=ar4sjg7u1g1t16cah2rjfkih3&code_challenge=V11qZ0ganE__op3krG3POUEYb5AV_-KiK_vRTordda4&code_challenge_method=S256&lang=ja&redirect_uri=https%3A%2F%2Fstaging.d3prbb8vtbfpjg.amplifyapp.com%2F&response_type=code&scope=email+openid+aws.cognito.signin.user.admin+profile&state=zARVByIx0HRLOde7n7I9LlaTAGyIfIcH&_data=root
```

特にパラメタ変更はありません。

| パラメータ名 | 値 | 説明 |
|------------|------|------|
| code_challenge | V11qZ0ganE__op3krG3POUEYb5AV_-KiK_vRTordda4 | PKCE認証用のコードチャレンジ |
| code_challenge_method | S256 | コードチャレンジに使用されるハッシュメソッド (SHA-256) |
| redirect_uri | https://staging.xxxxxxxxxx.amplifyapp.com/ | 認証後のリダイレクト先URL |
| response_type | code | 認証レスポンスタイプ |
| scope | email openid aws.cognito.signin.user.admin profile | 要求されるアクセス権限の範囲 |

### 認証成功後のリダイレクト

ログインに成功すると、`redirect_uri`に指定していたURIにリダイレクトされますが、このクエリパラメタに認可コードが付与されます。

```
https://staging.xxxxxxxxxx.amplifyapp.com/?code=2baa4995-88b8-44ed-b7bc-d0d894336ded&state=zARVByIx0HRLOde7n7I9LlaTAGyIfIcH
```

| パラメータ名 | 値 | 説明 |
|------------|------|------|
| code | 2baa4995-88b8-44ed-b7bc-d0d894336ded | 認可コード |

認可コードは一度限り利用できるコードで、次のトークン取得で即座に消費されるます。

ここで悪意のあるツールを利用し、他者が利用可能な認可コードを奪うことを「**認可コード横取り**」と言います。

### トークン取得

最後に認可コードから各種トークンを取得します。

```
https://ap-northeast-1nn5ct2qua.auth.ap-northeast-1.amazoncognito.com/oauth2/token
```

POST通信なので、リクエストボディは以下の通り。

```
grant_type=authorization_code&code=2baa4995-88b8-44ed-b7bc-d0d894336ded&client_id=ar4sjg7u1g1t16cah2rjfkih3&redirect_uri=https%3A%2F%2Fstaging.xxxxxxxxxx.amplifyapp.com%2F&code_verifier=3JLGEyr6ExmJNTWxKGWeWOcErTkhLh4DDz2pOBVDAbpSr1Dxe2yx0esP7l7qq2IZSjiA2JfngPVk0V4RBrRvzw6eCiHAdcMLFOqfCpi0dgcHeYaBOtoIfGLQsdswCwyH
```

抜粋して表にします。

| パラメータ名 | 値 | 説明 |
|------------|------|------|
| grant_type | authorization_code | トークン交換のタイプ（認可コードフロー） |
| code | 2baa4995-88b8-44ed-b7bc-d0d894336ded | 認証後に取得した認可コード |
| client_id | ar4sjg7u1g1t16cah2rjfkih3 | アプリケーションのクライアントID |
| redirect_uri | https://staging.xxxxxxxxxx.amplifyapp.com/ | 認可コード取得時と同じリダイレクトURI |
| code_verifier | 3JLGEyr6ExmJNTWxKGWeWOcErTkhLh4DDz2pOBVDAbpSr1Dxe2yx0esP7l7qq2IZSjiA2JfngPVk0V4RBrRvzw6eCiHAdcMLFOqfCpi0dgcHeYaBOtoIfGLQsdswCwyH | PKCE用のコード検証子 |

ここで`code_verifier`が出てきました。

つまり、認可コードから各種トークンを取得できるクライアントは、ハッシュ化前のランダムな文字列`code_verifier`を得るクライアント（＝認証開始時のクライアント）のみです。

これで認可コードを利用するクライアントは、認証を要求してきたクライアントであることを担保できます。

何かしらの方法で認可コードを横取りしても、`code_verifier`が分からないと各種トークンを取得できないのです。

なお、認可コード自体は各種トークンを取得する以外用途もありません。

無事取得に成功すると、以下の通りトークン取得できます。
```
{
    "id_token": "[IDトークンの値]",
    "access_token": "[アクセストークンの値]",
    "refresh_token": "[リフレッシュトークンの値]",
    "expires_in": 3600,
    "token_type": "Bearer"
}
```

### 値検証してみた。

最後に認証サービスがどうやって値の検証をしているかを確認してみました。

ロジックは以下のようです。

1. `code_verifier`をUTF-8エンコードしてバイト配列に変換
1. そのバイト配列のSHA-256ハッシュを計算（これはバイナリデータ）
1. ハッシュのバイナリデータをBase64エンコード
1. Base64をBase64URLに変換（「+」→「-」、「/」→「_」、末尾の「=」を削除）

Pythonコードにしてみました。

```py
import hashlib
import base64

def generate_code_challenge(code_verifier):
    # code_verifierをバイト列に変換
    verifier_bytes = code_verifier.encode('utf-8')
    
    # SHA-256ハッシュを計算
    hash_object = hashlib.sha256(verifier_bytes)
    hash_digest = hash_object.digest()
    
    # Base64エンコード
    base64_encoded = base64.b64encode(hash_digest)
    
    # Base64をBase64URL形式に変換（+ → -、/ → _、末尾の = を削除）
    base64_url = base64_encoded.decode('utf-8').replace('+', '-').replace('/', '_').rstrip('=')
    
    return base64_url

# 実行例
code_verifier = "3JLGEyr6ExmJNTWxKGWeWOcErTkhLh4DDz2pOBVDAbpSr1Dxe2yx0esP7l7qq2IZSjiA2JfngPVk0V4RBrRvzw6eCiHAdcMLFOqfCpi0dgcHeYaBOtoIfGLQsdswCwyH"
code_challenge = generate_code_challenge(code_verifier)
print(f"code_verifier: {code_verifier}")
print(f"code_challenge_method: S256")
print(f"code_challenge: {code_challenge}")
```

実行結果です。

無事、`code_verifier`から認証開始時の`code_challenge`が生成できています。

![スクリーンショット 2025-04-17 4.49.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/19a01e37-7764-4639-8514-a6a3347a6161.png)

### 最後に

実際に通信まで見ると理解が深まって良いですね〜！

試験で出てくれるといいな。