JWTのパースと署名・クレームの検証
目標
JWTを手で分解して署名を検証し、クレームを確認しながら、ライブラリが代わりにやってくれていたことが正確には何なのかを理解します。
なぜ重要なのか
JWTの検証はライブラリの1行で済むので、内部を知らないまま通り過ぎがちです。ところが、その1行にオプションを誤って指定すると、静かに突破されます。代表的なのがaudの検査を有効にしない場合で、同じ認証サーバーが発行した別のAPI向けのトークンが、私たちのAPIで通ってしまいます。algを検証しないと、noneアルゴリズムで署名のないトークンが通った事故が実際にありました。そして、このラボの最後に測る数字が重要です。introspectionはリクエストのたびにネットワークの往復が発生し、ローカル検証はCPUの演算です。マイクロサービス10個がそれぞれ問い合わせると認証サーバーがボトルネックになるため、実務ではアクセストークンを5–15分と短くして、ローカル検証を使います。取り消しの反映遅延をトークンの寿命の長さまでに抑える、という取引です。
ステップ
/root/kc/wait.shで、http://127.0.0.1:8080/realms/masterが200になるまで最大180秒待ってください。/root/kc/ready.txtにready_seconds=<정수>を書いてください(プレースホルダーは整数です)。/opt/fixtures/kc/realm-info.envの値でトークンを発行し、/root/kc/token.txtにアクセストークンだけを1行で保存してください。ドットが2つある文字列でなければなりません。/root/kc/decode.pyで、ヘッダーを/root/kc/header.jsonに、ペイロードを/root/kc/claims.jsonに保存してください。どちらも有効なJSONでなければなりません。/root/kc/claimcheck.txtにiss=<값> aud=<값> sub=<값> exp_in=<남은초>を書いてください(プレースホルダーは値と残りの秒数です)。exp_inは0より大きくなければなりません。http://127.0.0.1:8080/realms/labhub/protocol/openid-connect/certsからJWKSを取得し、ヘッダーのkidと一致する鍵を/root/kc/jwk.jsonに保存してください。ktyがRSAでなければなりません。/root/kc/verify.pyで署名を検証し、/root/kc/verify.outにsignature=valid alg=<값>を書いてください(プレースホルダーは値です)。- ペイロードを改ざんしたトークンで同じ検証を実行し、
/root/kc/tamper.outにsignature=invalidを書いてください。
参考
- Base64URLをデコードするときのパディングの補正:
s + '=' * (-len(s) % 4) - 署名の対象は、
<header_b64>.<payload_b64>の文字列そのままです。 - IDトークンとアクセストークンは別物です。API呼び出しにはアクセストークンを使います。
- よくあるミス1:
audを確認しないことです。別のAPI向けのトークンが通ります。 - よくあるミス2: ヘッダーの
algを確認しないことです。noneアルゴリズムによる回避の入り口になります。
Keycloakの準備ができるまで待つ
/root/kc/wait.shで、http://127.0.0.1:8080/realms/masterが200になるまで最大180秒待ってください。/root/kc/ready.txtにready_seconds=<정수>を書いてください(プレースホルダーは整数です)。
JVMなので、起動が遅いです。準備ができたかどうかをポーリングするループを使い、最大の待ち時間に余裕を持たせてください。
トークンを発行する
/opt/fixtures/kc/realm-info.envの値でトークンを発行し、/root/kc/token.txtにアクセストークンだけを1行で保存してください。ドットが2つある文字列でなければなりません。
事前にインポート済みのレルムの情報は、/opt/fixtures/kc/realm-info.envにあります。トークンエンドポイントにフォームデータを送ってください。
ヘッダーとペイロードをデコードする
/root/kc/decode.pyで、ヘッダーを/root/kc/header.jsonに、ペイロードを/root/kc/claims.jsonに保存してください。どちらも有効なJSONでなければなりません。
ドットで区切られた3つの断片のうち、最初の2つです。Base64URLはパディングがないことがあるので、補正が必要です。
必須クレームを確認する
/root/kc/claimcheck.txtにiss=<값> aud=<값> sub=<값> exp_in=<남은초>を書いてください(プレースホルダーは値と残りの秒数です)。exp_inは0より大きくなければなりません。
発行者、対象、有効期限、主体をそれぞれ確認します。どれか1つでも見ないと、穴になります。
JWKSから署名用の鍵を探す
http://127.0.0.1:8080/realms/labhub/protocol/openid-connect/certsからJWKSを取得し、ヘッダーのkidと一致する鍵を/root/kc/jwk.jsonに保存してください。ktyがRSAでなければなりません。
ヘッダーの鍵の識別子を使って、一覧から選びます。ローテーション中は、複数の鍵が同時にあることがあります。
署名を検証する
/root/kc/verify.pyで署名を検証し、/root/kc/verify.outにsignature=valid alg=<값>を書いてください(プレースホルダーは値です)。
署名の対象は、ヘッダーとペイロードをドットでつないだ文字列です。デコードしたJSONではありません。
改ざんしたトークンが拒否されることを確認する
ペイロードを改ざんしたトークンで同じ検証を実行し、/root/kc/tamper.outにsignature=invalidを書いてください。
ペイロードの1文字だけを変えても、署名が壊れるのが正常です。