authorization code + PKCEの流れを完走する
目標
authorization code + PKCEのフローを、SDKなしで最初から最後まで実行し、PKCEとstateがそれぞれ何を防ぐのかを、失敗するケースで確認します。
なぜ重要なのか
OIDCライブラリを使うと、このフローは関数呼び出し2回で済みます。そのため、何が起きているかを知らないまま使うことになり、問題が起きたときにどこを見ればよいかがわかりません。このラボで各ステップを手で行うと、3つのことがはっきりします。1つ目は、認可コードがブラウザーのURLに露出するのに、なぜそれだけでは役に立たないのか。2つ目は、code_verifierを知らないと、なぜ交換が失敗するのか(ステップ6で自分の手で失敗させてみます)。3つ目は、stateがないと、なぜ攻撃者が被害者を自分のアカウントにログインさせられるのか。最後のリフレッシュのローテーションまで見れば、実務で推奨される組み合わせ(短いアクセストークン + リフレッシュのローテーション)が、なぜ拒否リストなしでも無効化の効果を生むのかが理解できます。
Keycloakの起動には40–90秒かかるので、前のラボを終えてから来れば、たいてい準備ができています。
ステップ
/root/pk/pkce.pyで、code_verifier(43–128文字、URLセーフな文字)とcode_challengeを作成し、/root/pk/verifier.txtと/root/pk/challenge.txtにそれぞれ1行ずつ保存してください。/root/pk/authz_url.txtに、認可リクエストのURLを1行で書いてください。対象は、事前にインポートされたレルムlabhubのパブリッククライアントweb-appで、リダイレクトURIはhttp://127.0.0.1:8161/callbackです。response_type=code、client_id、redirect_uri、scope=openid、state、code_challenge、code_challenge_method=S256がすべて含まれていなければなりません。- クッキージャーを維持しながらそのURLをGETし、ログインフォームの
actionのURLを/root/pk/action.txtに保存してください。httpで始まっていなければなりません。 - ユーザー
dev1/パスワードDev1!passをPOSTし、リダイレクトのレスポンスのLocationから認可コードを取り出して、/root/pk/code.txtに保存してください。20文字以上でなければなりません。 - トークンエンドポイントに、
grant_type=authorization_code、コード、redirect_uri、client_id、code_verifierを送ってトークンを受け取ってください。/root/pk/tokens.jsonにaccess_tokenとrefresh_tokenがなければなりません。 - 新しい認可コードを受け取り、間違った
code_verifierで交換を試みてください。/root/pk/pkce_fail.jsonのerrorがinvalid_grantでなければなりません。 /root/pk/state_check.pyは、引数を2つ(送ったstate、受け取ったstate)受け取り、同じなら終了コード0、違えば1で終了するようにしてください。一致する場合と一致しない場合の2つの結果を、/root/pk/state.outにmatch=ok mismatch=rejectedの形式で書いてください。- リフレッシュトークンで更新して、
/root/pk/refresh.jsonを作成してください。/root/pk/rotation.txtにrotated=<true|false>を書いてください。判断の根拠は、新しいリフレッシュトークンが以前のものと違うかどうかです。
参考
- challengeの計算:
base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b'=') - クッキーの維持:
curl -c /root/pk/cookies -b /root/pk/cookies ... - リダイレクトを追跡しないことで、Locationヘッダーが見えます(
-iを付け、-Lは付けません)。 - よくあるミス1: ハッシュ結果を16進文字列にしてからBase64にすることです。バイナリのダイジェストをエンコードしなければなりません。
- よくあるミス2: トークン交換の
redirect_uriが認可リクエストのときと違って、invalid_grantになることです。
code_verifierとchallengeを作る
/root/pk/pkce.pyで、code_verifier(43–128文字、URLセーフな文字)とcode_challengeを作成し、/root/pk/verifier.txtと/root/pk/challenge.txtにそれぞれ1行ずつ保存してください。
長さの制限と文字の集合が、規格で決められています。ハッシュ結果をそのままBase64にしてはいけません。URLセーフなエンコーディングにして、パディングを取り除く必要があります。
認可リクエストのURLを組み立てる
/root/pk/authz_url.txtに、認可リクエストのURLを1行で書いてください。対象は、事前にインポートされたレルムlabhubのパブリッククライアントweb-appで、リダイレクトURIはhttp://127.0.0.1:8161/callbackです。response_type=code、client_id、redirect_uri、scope=openid、state、code_challenge、code_challenge_method=S256がすべて含まれていなければなりません。
必須のパラメータが6つあります。1つでも欠けると、認証サーバーがエラーを返します。
ログインフォームのactionを取り出す
クッキージャーを維持しながらそのURLをGETし、ログインフォームのactionのURLを/root/pk/action.txtに保存してください。httpで始まっていなければなりません。
認可エンドポイントをGETすると、HTMLが返ります。クッキーを維持しないと、次のステップが通りません。
資格情報をPOSTしてコードを受け取る
ユーザーdev1/パスワードDev1!passをPOSTし、リダイレクトのレスポンスのLocationから認可コードを取り出して、/root/pk/code.txtに保存してください。20文字以上でなければなりません。
フォームのフィールド名を、HTMLで確認してください。成功すると、リダイレクトのレスポンスの位置を示すヘッダーにコードが入っています。
コードをトークンに交換する
トークンエンドポイントに、grant_type=authorization_code、コード、redirect_uri、client_id、code_verifierを送ってトークンを受け取ってください。/root/pk/tokens.jsonにaccess_tokenとrefresh_tokenがなければなりません。
認可コードと元のverifierを一緒に送ります。リダイレクトURIも、最初のものと正確に同じでなければなりません。
間違ったverifierで失敗することを確認する
新しい認可コードを受け取り、間違ったcode_verifierで交換を試みてください。/root/pk/pkce_fail.jsonのerrorがinvalid_grantでなければなりません。
これが、PKCEが実際に機能している証拠です。エラーコードを記録してください。
stateの不一致の検証を実装する
/root/pk/state_check.pyは、引数を2つ(送ったstate、受け取ったstate)受け取り、同じなら終了コード0、違えば1で終了するようにしてください。一致する場合と一致しない場合の2つの結果を、/root/pk/state.outにmatch=ok mismatch=rejectedの形式で書いてください。
認可リクエストで送った値と、コールバックで戻ってきた値を比較します。違えば、拒否しなければなりません。
リフレッシュとローテーションを確認する
リフレッシュトークンで更新して、/root/pk/refresh.jsonを作成してください。/root/pk/rotation.txtにrotated=<true|false>を書いてください。判断の根拠は、新しいリフレッシュトークンが以前のものと違うかどうかです。
更新すると、新しいリフレッシュトークンが返ってきます。古いものと同じか違うかが、ローテーションの有無です。