OAuth・OIDCとPKCE
コードを持つだけでは交換できない理由と、各トークンを渡す相手を説明します。
説明
OAuthの認可コードは、クライアントがトークンへ交換する短命な値です。PKCEでは要求時のchallengeと交換時のverifierを結び付けます。OpenID ConnectのID tokenと、APIへ提示するaccess tokenは利用目的が違います。
RFCのverifierからS256 challengeを計算し、コード交換の失敗と成功を比較します。受け取った二種類の署名付きトークンを検証し、audience、nonce、scopeの働きを調べます。
コードを持つだけでは交換できない理由と、各トークンを渡す相手を説明します。
1verifierからchallengeを計算する
S256はverifierのSHA-256を、パディングなしBase64urlで表します。元のverifierはコード交換までクライアントが保持します。
from tokens import *
print(VERIFIER)
print(challenge(VERIFIER)S256 challengeを計算する)
auth = Authorization()code-7にchallengeを結び付けるRFCの試料ではchallengeがE9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cMになります。challengeからverifierを単純に復号する方式ではありません。
2コードだけを持つ相手の交換
正しいコードに、別のverifierを付けて交換します。
print(auth.exchange("code-7","wrong-verifier")コードと不一致のverifierを提示する)
print(auth.used)pkce_mismatchで、コードは未使用のままです。要求時に結び付けたverifierが必要です。
3redirect URIも照合する
正しいverifierを持っていても、コードに結び付けた返送先と違う値は拒否します。
print(auth.exchange("code-7",VERIFIER,"https://other.example.test/cb"交換時の返送先を変更する))
print(auth.used)redirect_mismatchです。PKCEがredirect URIの照合を代替するわけではありません。
4正しい組で一度だけ交換する
正しいverifierと登録した返送先で交換します。コードは成功時に消費します。
tokens = auth.exchange正しい条件でコードを交換する("code-7",VERIFIER)
print(sorted(tokens))
print(auth.exchange同じコードの再利用を試す("code-7",VERIFIER))id_tokenとaccess_tokenを受け取り、再交換はinvalid_grantです。この例ではトークンをEd25519で署名しています。
5ID tokenの受信者とnonceを検証する
ID tokenのaudはweb-client、nonceは開始した認証要求のnonce-7です。署名検証の後で照合します。
print(claims(tokens["id_token"],"web-client",nonce="nonce-7"開始した要求のnonceと照合する))
print(claims(tokens["id_token"],"web-client",nonce="nonce-8"別要求のnonceと比較する))正しいnonceならaliceの認証情報を取得でき、不一致はnonce_mismatchです。Base64urlを展開して読めるだけでは署名検証をしたことにはなりません。
6APIにID tokenを渡す
APIは自分をaudienceとするaccess tokenを期待します。
print(api(tokens["id_token"]ID tokenをAPIへ提示する,"orders:read"))
print(api(tokens["access_token"]API向けトークンを提示する,"orders:read"))ID tokenはaudience_mismatch、access tokenはallowedです。aliceという同じsubがあるだけで相互に代用できません。
7scopeと期限を検査する
API向けであっても、許可する操作と期限を検査します。時刻の単位は固定試料の基準時刻からの秒です。
print(api(tokens["access_token"],"orders:write"与えられていない操作を要求する))
print(api(tokens["access_token"],"orders:read",now=300有効期限の境界で検証する))書込みはinsufficient_scope、期限境界はexpiredです。署名が正しいトークンにも利用範囲があります。
まとめ
PKCEはコードを要求時のverifierへ結び付けます。
ID tokenとaccess tokenの受信者・目的は異なります。
署名に加え、issuer・audience・期限・要求との対応を検査します。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from tokens import *
print(VERIFIER)
print(challenge(VERIFIER))
auth = Authorization()dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
print(auth.exchange("code-7","wrong-verifier"))
print(auth.used)pkce_mismatch False
print(auth.exchange("code-7",VERIFIER,"https://other.example.test/cb"))
print(auth.used)redirect_mismatch False
tokens = auth.exchange("code-7",VERIFIER)
print(sorted(tokens))
print(auth.exchange("code-7",VERIFIER))['access_token', 'id_token'] invalid_grant
print(claims(tokens["id_token"],"web-client",nonce="nonce-7"))
print(claims(tokens["id_token"],"web-client",nonce="nonce-8")){'iss': 'https://idp.example.test', 'sub': 'alice', 'aud': 'web-client', 'iat': 0, 'exp': 300, 'nonce': 'nonce-7'}
nonce_mismatchprint(api(tokens["id_token"],"orders:read"))
print(api(tokens["access_token"],"orders:read"))audience_mismatch allowed
print(api(tokens["access_token"],"orders:write"))
print(api(tokens["access_token"],"orders:read",now=300))insufficient_scope expired
演習完了