一覧SC-10-2 · SSOと認証連携

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 / 公開試料と固定時刻

1
from tokens import * print(VERIFIER) print(challenge(VERIFIER)) auth = Authorization()
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
2
print(auth.exchange("code-7","wrong-verifier")) print(auth.used)
pkce_mismatch
False
3
print(auth.exchange("code-7",VERIFIER,"https://other.example.test/cb")) print(auth.used)
redirect_mismatch
False
4
tokens = auth.exchange("code-7",VERIFIER) print(sorted(tokens)) print(auth.exchange("code-7",VERIFIER))
['access_token', 'id_token']
invalid_grant
5
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_mismatch
6
print(api(tokens["id_token"],"orders:read")) print(api(tokens["access_token"],"orders:read"))
audience_mismatch
allowed
7
print(api(tokens["access_token"],"orders:write")) print(api(tokens["access_token"],"orders:read",now=300))
insufficient_scope
expired

演習完了