はじめに
下記リンクの通り、CognitoはOIDCの認証フローの中で認可コードを発行する処理にPKCEが利用できます。
ちょうど、情報処理安全支援確保士試験の勉強しているので、実際の通信を覗いてみたいと思います。
PKCE(ピクシー)とは?
SPAのようなクライアントサイドで動作するWEBやアプリは認証コードの横取りを防ぐ必要があり、この仕組みがPKCEになります。
詳細は以下サイトをご確認ください。
通信を覗いてみる。
認証開始時
マネージドログイン画面を表示したタイミングで処理開始かな?と思ってたのですが、実際はID(今回はメールアドレス)を入れた時に認証が開始されます。
クエリパラメタは以下の通りです。
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等の仕様の模様です。
同様にクエリパラメタを確認します。
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"
}
値検証してみた。
最後に認証サービスがどうやって値の検証をしているかを確認してみました。
ロジックは以下のようです。
-
code_verifier
をUTF-8エンコードしてバイト配列に変換 - そのバイト配列のSHA-256ハッシュを計算(これはバイナリデータ)
- ハッシュのバイナリデータをBase64エンコード
- Base64をBase64URLに変換(「+」→「-」、「/」→「_」、末尾の「=」を削除)
Pythonコードにしてみました。
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
が生成できています。
最後に
実際に通信まで見ると理解が深まって良いですね〜!
試験で出てくれるといいな。