はじめに
第3回の最後で、こんな話をして終わりました。
「ID トークンは、ハンコ(署名)を確認してから信じる」。
ところがこのトークン、jwt.io のようなサイトに貼り付けると、誰でも中身が読めてしまいます。
読めてしまうのに、なぜ「安全」と言えるのか。
そもそも、あのハンコはどこに押されているのか。
今回はそれを、長い文字列をバラバラに分解しながら追いかけます。
この記事でわかること
- あの長い文字列がどういう部品でできているのか、そして「読める」のに安全と言える理由
- ハンコが押されているのは「どの紙」なのか。自前実装が必ず転ぶポイント
- トークンに書いてある
algを信じてはいけない理由と、検証に失敗したときの切り分け順
この記事の前提
- 対象は、
jwt.ioにトークンを貼って眺めたことはあるが、署名検証の意味がふわっとしている人 - 第2回「OAuth 2.0とは?」(
Authorization: Bearer)と
第3回「OIDCとは?」(iss/sub/aud/expと JWKS)の内容を前提にします - Base64URL や公開鍵は本文で一から説明するので、事前知識は不要です
本編
まず、トークンって何だったか
用語メモ「トークン」
認証を通った人に発行される、「誰であるか」「何をしてよいか」が書かれた券のことです。
一度受け取れば、あとは券を見せるだけで通してもらえます。
イメージはイベント会場の入場リストバンド。
問題は、受け取った側が「これ、本物?」をどう判断するかです。
やり方は大きく2つあります。
| やり方 | 例え | 特徴 |
|---|---|---|
| 発行元に毎回問い合わせる | 受付に電話して「この番号の人、登録されてます?」と聞く | 確実だが、毎回の問い合わせが必要 |
| 券そのものを見て判断する | 券面の割印を見て、その場で判断する | 問い合わせ不要。ただし「回収して無効にする」ができない |
JWT は後者です。
トークン単体で「本物か」が決まる。
この性質を 自己完結 と呼びます。
そして「JWT は即時失効ができない」という弱点は、この自己完結性の裏返しです。
問い合わせに行かないからこそ速く、問い合わせに行かないからこそ「もう無効です」と伝えられない。
利点と欠点が同じ性質から出ているので、いいとこ取りはできません。
JWT は3つの部品でできている
JWT はドット(.)で区切られた3つのカタマリです。
eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYyJ9.eyJzdWIiOiIxMjM0In0.<署名>
証明書にたとえると、こうなります。
| パート | 中身 | たとえると |
|---|---|---|
| ① Header | 「このハンコは○○という方式で、××番の印鑑を使いました」 | 券面の欄外の注記 |
| ② Payload | 「この人は誰で、いつまで有効で、誰宛てか」 | 券面の本文 |
| ③ Signature | 改ざんされていないことを示す値 | 割印・ハンコそのもの |
用語メモ「JWT(ジョット)」
JSON Web Token の略。
「主張(クレーム)を JSON で書いて、コンパクトに持ち運ぶための形式」です。
実務で見かける JWT はほぼ JWS(JSON Web Signature)という署名付きの形です。
「JWT = 署名付きの券」と思って大丈夫です。
①Header と ②Payload は、誰でも読めます。
隠されていません。
守られているのは「読まれないこと」ではなく、書き換えられたら気づけること です。
①②が読めるのに安全、のカラクリ
eyJhbGciOiJSUzI1NiJ9 のような呪文めいた文字列の正体はBase64URL、つまりJSON を「英数字と - _ だけ」で書き直したものです。
パスワードも鍵も要らず、誰でも一瞬で元の JSON に戻せます。
| 用語 | 何をするもの | 元に戻すのに必要なもの |
|---|---|---|
| エンコード(Base64URL) | 表記を書き換えるだけ | 何も要らない(誰でも戻せる) |
| 暗号化 | 鍵を知らない人には読めなくする | 鍵 |
Base64URL はエンコードであって、暗号化ではありません。
ここを取り違えると、「JWT は暗号化されているから個人情報を入れても平気」という致命的な設計ミスにつながります。
普通の Base64 との差は、次の3点です。
- 記号が
+/ではなく-_ - 末尾の埋め文字
=を付けない - 途中で改行しない
どれも、URL や Cookie や HTTP ヘッダーにそのまま載せたいからです。
+ は URL 内で半角スペースと解釈されることがあり、/ はパス区切り、= は名前と値の区切りとぶつかる。
改行はヘッダーに載せた時点で壊れます。
厳密には、Base64URL(RFC 4648 の §5)の決まりは記号2文字だけ。
= と改行を落とすのは JWT 側(RFC 7515 の §2)のルールです。
ここは実装でハマるポイント
=が無いぶん、一般的な Base64 デコーダはそのままだとエラーを出すことがあります。
手で=を補ってください(あとで実際に踏みます)。
逆に自分でエンコードするときは改行に注意。
Linux のbase64はデフォルトで76桁ごとに折り返すので、-w0で止めます(macOS は既定で折り返しません)。
用語メモ「エンコード」
中身は変えずに、表記のしかただけを別のルールで書き直すことです。
日本語をローマ字で書き直すようなもので、ルールを知っていれば誰でも元に戻せます。
「隠す」操作ではないので、秘密を守る役には立ちません。
① Header に書いてあるのは「照合のしかた」
| 項目 | 意味 | 例えると |
|---|---|---|
alg |
ハンコの方式(RS256 など) |
「実印です」「シャチハタです」 |
kid |
ハンコの個体識別番号 | 「印鑑証明の何番の印です」 |
kid(キーID)のうれしさは、ハンコを作り直しても古いハンコの券が無効にならないこと。
kid が無ければ、鍵を新しくした瞬間に発行済みの全トークンが一斉に検証エラーになります。
kid に対応する公開鍵は、第3回の JWKS(印鑑の見本帳)から取ってきます。
用語メモ「typ」
Header にtypという項目が入っていることがあります。
「これは何用の JWT か」を宣言するもので、
ID トークンをアクセストークンとして受け取ってしまう取り違え事故を防ぐために使います。
ただしtypは宣言でしかありません。
受け取る側が「自分が期待する型か」を突き合わせて初めて防御になります。
AWS を使っている人向けメモ
Cognito が発行するトークンの Header はkidとalgの2つだけで、typは入りません。
「Header にはtypがある」と書かれた記事もありますが、typの有無で判定するコードは Cognito のトークンで落ちます。
用途(ID 用かアクセス用か)の判定はtoken_useクレームで行います(第3回)。
② Payload に並ぶ「主張」
Payload の項目一つひとつを クレーム(Claim) と呼びます。
「苦情」ではなく、「〜である」という主張のことです。
| 種類 | 例 | 説明 |
|---|---|---|
| 登録済みクレーム |
iss / sub / aud / exp / iat / jti
|
仕様で意味が決まっているもの |
| 公開クレーム |
email / name
|
OIDC などの標準で意味が決まっているもの |
| プライベートクレーム |
role / tenant_id
|
発行側と検証側の取り決めで決まる独自項目 |
各クレームの意味は第3回で扱ったので、ここでは最初に踏む地雷を1つだけ。
時刻は全部「UNIX 秒」です。
exp: 1754003600は1970年1月1日からの経過秒数であり、ミリ秒ではありません。
ミリ秒だと思って扱うと期限がとんでもない未来になり、期限切れを永久に検出できません。
「なぜか期限切れトークンが通ってしまう」の典型原因です。
③ Signature のハンコは「どの紙」に押されているのか
ここが本記事の心臓部です。
素直に考えると「デコードした後の JSON にハンコを押しているんでしょ?」と思いますよね。
違います。
ハンコが押されているのは、受け取ったままの Base64URL 文字列です。
①Header の文字列 + "." + ②Payload の文字列
この、呪文めいた見た目のままの状態を 署名対象(Signing Input) と呼びます。
なぜ「読める JSON」ではないのか。
パスワードを思い出してください。
大文字を小文字で打っただけで弾かれますよね。
「意味は合っているのに」は通用しない。
署名の照合もこれと同じで、文字の並びが1つでも違えば別物になります。
そして JSON には「同じ意味なのに見た目が違う」書き方がいくらでもあります。
発行元が署名したときの並び。
{"a":1,"b":2}
検証側が JSON を読み直して組み立てた並び。
{
"b":2,
"a":1
}
人が読めば同じ内容です。
でも署名の計算は、この文字列をまるごと短い値に潰すところから始まります。
実際に計算してみると、こうなります。
| 並び | 計算結果(SHA-256 の先頭16文字) |
|---|---|
| 1行のほう | 43258cff783fe703 |
| 整形したほう | f81b8c3f75b9a737 |
1文字も重なっていません。
しかもこれは、大きく書き換えたからではありません。
{"a":1,"b":2} の 2 を 3 に変えるだけでも、結果は f9c6777fb8659792 になります。
1つ違えば少し違う、ではなく、1つ違えば全部違う。
だから JWT は、受け取った文字列を、そのままハンコの対象にする と決めました。
組み立て直した JSON で照合が通ることはありません。
ここが自前実装で必ず転ぶポイント
「JSON をパースして、署名用にもう一度 JSON に組み立て直す」。
これをやると、キーの順番や空白がズレて、署名は100%不一致になります。
「ライブラリを使わず自分で検証を書いたら、なぜか毎回失敗する」の原因は、たいていこれです。
検証側は、受け取った元の文字列を保持したまま扱う必要があります。
ハンコの押し方は2種類ある
| 観点 | HS256(HMAC)= 合鍵 | RS256(RSA 署名)= 実印と印鑑証明 |
|---|---|---|
| 鍵 | 共有の秘密鍵1つ(押す人も照合する人も同じ鍵) | 秘密鍵で署名 / 公開鍵で検証 |
| 検証する側が必要なもの | 秘密鍵そのもの | 公開鍵だけ |
| 「本物」を作れるのは | 鍵を持っている人全員 | 秘密鍵の持ち主だけ |
| 向く場面 | 発行者と検証者が同じシステム | 発行者と検証者が別(IdP と複数の API) |
| リスク | 鍵を配るほど漏洩する面が増える | 鍵配布は JWKS で自動化できる |
用語メモ「公開鍵と秘密鍵」
ペアになった2つの鍵です。
秘密鍵で押したハンコは、対になる公開鍵で「本物だ」と確認できる。
しかも公開鍵からは秘密鍵を割り出せません。
だから公開鍵は世界中に公開してよい。
それが JWKS(印鑑の見本帳)です。
Cognito のような認証基盤(IdP)が RS256 を選ぶ理由は、HS256 では「検証できる」と「発行できる」が同じ意味になってしまう から。
API が10個あって全部に共有鍵を配ったら、IdP になりすませる主体が10個に増えたということです。
【注意】
「HS256 のほうが速いから」で選ばないでください。
軸は速度ではなく、鍵をどこまで配ることになるかです。
この2つ以外にも、ES256(楕円曲線を使う方式。署名が短くて済む)や PS256(同じ RSA 鍵を使う、より新しい方式)があります。
仕様が「必ず実装すること」としているのは HS256 だけで、RS256 と ES256 は推奨止まり。
ただし Cognito は RS256 固定なので、AWS を使っている限り選ぶ場面はまずありません。
ハンコが保証すること・しないこと
| 内容 | |
|---|---|
| 保証する | ①Header と ②Payload が発行時から1文字も変わっていないこと/発行者が鍵を持っていること |
| 保証しない | 中身の秘密(誰でも読める)/期限切れでないこと(exp の確認は別作業)/自分宛てであること(aud の確認は別作業)/盗まれた券の再利用を防ぐこと |
「署名が通った」は 「改ざんされていない」までしか言っていません。
「使ってよい」は別途、自分で確かめる必要があります。
実際に分解してみる
「JWT は3つの部品でできている」で出した、この例を分解していきます。
eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYyJ9.eyJzdWIiOiIxMjM0In0.<署名>
(1) 1つ目のカタマリ(Header)を戻してみる
echo -n 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYyJ9' | base64 -d
# => {"alg":"RS256","kid":"abc"}
鍵もパスワードも使っていません。
これが「暗号化ではない」の実演です。
(2) 2つ目のカタマリ(Payload)で、例のパディング問題を踏む
2つ目のカタマリを、そのままデコードしてみます。
echo -n 'eyJzdWIiOiIxMjM0In0' | base64 -d
# => {"sub":"1234"}base64: invalid input
echo $?
# => 1
JSON は最後まで正しく出るのに、終了コードだけ 1 になります。 この文字列は19文字で、4の倍数ではないからです。
Payloadの末尾に = を1つ補ったeyJzdWIiOiIxMjM0In0=にして再実行すれば、終了コード 0 で通ります。
終了コードを見ていないと「たまたま動いている」状態に気づけません。
(3) 実物の Payload は、もっと太い
ここまで分解してきた例は、説明用に短くしてあります。
Cognito が実際に返す ID トークンの Payload は、こういう中身です。
echo -n 'eyJzdWIiOiIxMjM0IiwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5hcC1ub3J0aGVhc3QtMS5hbWF6b25hd3MuY29tL2FwLW5vcnRoZWFzdC0xX2FiMTJjZDM0ZSIsImF1ZCI6IjdmOWMyYmE0ZTg4ZiIsInRva2VuX3VzZSI6ImlkIiwiaWF0IjoxNzU0MDAzMDAwLCJleHAiOjE3NTQwMDM2MDB9' | base64 -d
# => {"sub":"1234","iss":"https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_ab12cd34e",
# "aud":"7f9c2ba4e88f","token_use":"id","iat":1754003000,"exp":1754003600}
iss / sub / aud / token_use / iat / exp が、鍵なしで全部読めました。
実物にはさらに cognito:groups などが加わってもっと長くなります。
この「実物は太い」が後の話につながります。
なお、3つ目(Signature)はバイナリなので、デコードしても読める文字にはなりません。
(4) 中身を書き換えてみると?
Payload の "token_use":"id" を "access" に書き換え、署名は元のまま付け替えて送ります。
結果は、デコードは通る(JSON としては壊れていない)が、署名の照合だけが落ちる。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="signature verification failed"
これが 「デコードできた ≠ 検証できた」 の正体です。
署名の照合を省いた実装なら、この改ざんトークンはそのまま通ります。
なお
error="invalid_token"は仕様(RFC 6750)で決まっていますが、error_descriptionの文言は決まっていません。
エラーメッセージの文字列一致で処理を分岐させないでください。
検証の「正しい順番」
| # | やること |
|---|---|
| 1 | 自分が許可する方式を、自分で決めて固定する(トークンの alg に従わない) |
| 2 |
kid を見て、JWKS から照合用の鍵を選ぶ |
| 3 | 署名を照合する |
| 4 |
iss / aud / exp などのクレームを確認する |
なぜこの順番なのか。
署名の照合が終わるまで、Payload はただの文字列 だからです。
たとえば「まず iss を見て、発行元ごとに処理を分ける」実装は、攻撃者が自由に書き換えられる値で処理を分岐させていることになります。
順番が逆です。
なお exp の判定にはサーバ間の時計のズレという現実問題があります。
ズレを一切許さない設定にすると数秒のズレで散発的に検証が落ち、再現しない障害になります。
かといって許容幅を広げすぎると、期限切れトークンが通る時間がその分だけ延びます。
exp を数分など短く設定している場合、許容幅が相対的に大きくなる点に注意してください。
いちばん危ない勘違い 「alg に従えばいい」
この記事で1つだけ持ち帰るとしたら、ここです。
Header には alg(照合の方式)が書いてある。
だったら素直にそれに従えばいい。
これが最悪の実装です。
Header は攻撃者が自由に書き換えられる からで、
つまりこれは、
「どうやって照合するか」を、トークンを送ってきた本人に決めさせている
ということ。
銀行の窓口で「照合方法はお客様がお決めください」と言っているのと同じです。
攻撃1 「署名なし」を指定する(alg: none)
「署名なし」を指定して ③Signature を空にしたトークンを送る。
alg に素直に従う実装は「署名なしって書いてあるから照合不要ですね」と通してしまいます。
攻撃2 アルゴリズムを混同させる
こちらのほうが巧妙です。
RS256 は「押すのは秘密鍵、照合は公開鍵」ですが、HS256 は「押すのも照合も同じ鍵」。
攻撃者は「HS256 でお願いします」と指定したうえで、誰でも手に入る公開鍵を合鍵として使ってハンコを押す。
API 側も同じ公開鍵を合鍵として照合するので、一致してしまいます。
対策は「公開鍵を隠す」ではありません(そもそも隠せません)。
検証する側が、許可する方式を自分で固定する。
「うちは RS256 しか受け付けない」と決め打ちする。
これだけで両方の攻撃が止まります。
やってはいけない実装リスト
| # | やってはいけないこと | 何が起きるか | どうすればよいか |
|---|---|---|---|
| 1 |
alg を信じて照合方式を切り替える |
上記の2つの攻撃がそのまま通る | 検証側で許可する方式を固定する(許可リスト) |
| 2 | 署名の照合を省いて Payload だけデコードして使う | 誰でも自作トークンを通せる |
decode(読むだけ)ではなく verify(照合する)を使う |
| 3 | 有効期限を確認しない | 一度漏れたトークンが永久に使える |
exp を必ず確認する(UNIX 秒であることに注意) |
| 4 | JWKS を毎回取りに行く/永久にキャッシュする | 前者は遅くて壊れやすく、後者は鍵の入れ替え時に一斉に検証失敗する | 適度にキャッシュしつつ、未知の kid が来たら取り直す(実装は第8回) |
| 5 | 署名の一致を == で比べる |
応答時間の差から署名を1文字ずつ当てられる余地が残る | ライブラリの verify を使う。中で処理時間が一定になる比較をしている |
| 6 | 本番のトークンを jwt.io などの外部サイトに貼る |
中身が第三者のサイトに送信される | デバッグは手元のデコードで行う |
| 7 | トークンをログや URL のクエリ文字列に出す | アクセスログや監視サービスに平文で残り、期限切れまで再利用できる |
Authorization ヘッダーで送り、ログではマスクする |
5番「署名の一致を == で比べる」の補足
5番だけ補足します。
「時間を測ると署名が分かる」がピンとこないと思うので、順に見ます。
== は先頭から1文字ずつ比べて、違うところが見つかった時点で止まります。だから、
- 1文字目から違う → すぐ終わる
- 1文字目は合っていて2文字目で違う → ほんの少しだけ長くかかる
攻撃者はここを突きます。
1文字目を64通り(Base64URL で使える文字の数)すべて試し、いちばん応答が遅かったものを選ぶ。
それが正解の1文字目です。
次は1文字目を固定して、2文字目を64通り試す。
これを繰り返せば、署名を先頭から1文字ずつ確定できてしまいます。
怖いのは試行回数です。
HS256 の署名は Base64URL で43文字あるので、まともに総当たりすれば 64 の43乗通り。
ところが1文字ずつ確定していけば 43 × 64 = 2,752 回で済みます。
不可能だったものが、現実的な回数に変わる。
対策は「違うところを見つけても、最後まで比べる」比較です。
常に全文字を比べるので時間差が出ません。
ライブラリの verify はこれをやってくれるので、自前で == を書かないことだけ覚えておけば十分です。
なお、この話が効くのは HS256 のほうです。
RS256 は公開鍵で署名を検証するので、そもそも「正解の署名文字列と見比べる」という処理をしません。
検証に失敗したときの切り分け
「検証エラー」の一言で片付けず、どの段階で落ちたかを先に特定します。
| 症状 | 疑う原因 | 確認すること |
|---|---|---|
| ある時刻から全リクエストが失敗 | 鍵の入れ替えと JWKS キャッシュのズレ | Header の kid が JWKS に存在するか |
| 一部のユーザーだけ失敗 | 別のユーザープール/App Client のトークンが混ざっている | Payload の iss と aud を実際の値で突き合わせる |
| ローカルは通るが本番で失敗 | 期待する iss の設定違い、環境変数の取り違え |
環境ごとの設定値を並べて比べる |
| いつも署名不一致 | 署名対象を組み立て直している | 受け取った Base64URL 文字列をそのまま使っているか |
| いつも形式エラー | コピペ時の改行・空白混入、パディングの扱い | ドットが2つあるか、各パートの文字数 |
| 散発的に期限エラー | サーバ間の時計のズレ | 時刻同期と許容幅の設定 |
上3行は「誰が失敗するか」で切り分ける行、下3行は「いつ失敗するか」で切り分ける行です。
まず「全員か・一部か」「常時か・散発か」の2軸で当たりを付ける。
これだけで確認対象が一気に絞れます。
AWS を使っている人向けメモ
検証を ALB のリスナールール(JWT 認証)に任せている場合は、ALB が失敗理由を名前付きで返してくれます。アクセスログの error reason code が
JWTHeaderNotPresent(Authorizationヘッダーがない)、
JWTRequestFormatInvalid(3パートが揃っていない/Bearer接頭辞がない)、
JWTSignatureValidationError(署名不一致/kidが JWKS にない)、
JWTClaimNotPresent(必要なクレームが欠けている)のように、上の表の切り分け段階とほぼ対応します。「鍵を取りに行けなかった」場合は
562 JWKS Request Failedという別のコードになるので、署名検証の失敗と鍵取得の失敗をステータスコードだけで分離できます。
クレーム側(iss / aud / nonce の不一致)の切り分けは第3回、
アプリ側(API Gateway / Django / FastAPI)での切り分けは第8回で扱います。
何を入れるか・どこに置くか
繰り返しになりますが、中身は誰でも読めます。
| 例 | |
|---|---|
| 入れてよい | 識別子(sub)、権限(ロール・スコープ)、有効期限 |
| 入れてはいけない | パスワード、クレジットカード番号、他人に見られて困る個人情報 |
保管についても、押さえるのは3点だけです。
-
localStorageに置くと、XSS(攻撃者のスクリプトが自分のサイト上で実行される脆弱性)で JavaScript から読み出される。Cookie にHttpOnlyを付けるとスクリプトから読めなくなる - ログ・URL のクエリ文字列・エラー通知に、トークンをそのまま出さない
- 短い有効期限が、保管の失敗に対する最後の保険。 署名では盗まれた券の再利用を防げず、失効もできない以上、有効時間を短くするしかありません
もう1つの制約はサイズの上限
JWT は HTTP ヘッダーや Cookie に載って飛ぶので、太りすぎるとリクエストが届かなくなります。
| 経路 | 上限 | 超えるとどうなるか |
|---|---|---|
| API Gateway(HTTP API) | リクエストラインとヘッダー値の合計 10240 バイト(引き上げ不可) | 手前で弾かれてアプリに届かない(REST API では 431 Request Header Fields Too Large) |
| Application Load Balancer | リクエストライン 16 K / 単一ヘッダー 16 K / ヘッダー全体 64 K |
400 Bad request または 413 Payload too large
|
| Cognito ユーザープール | カスタム属性は1プールあたり 50 個(引き上げ不可) | 51 個目の作成が失敗する |
太る原因のほとんどは、所属グループやカスタム属性を全部トークンに載せる設計です。
見え方も厄介で、属性の多い一部ユーザーだけログインできず、手前のゲートウェイで落ちるのでアプリのログには何も残りません(HTTP API には実行ログ自体が無く、アクセスログでしか追えない)。
返るコードも経路で違うため、400 を見て「クライアントが変なリクエストを投げた」と決めつけると真因に永久にたどり着けません。
対処は、トークンには識別子だけ載せて、詳細はサーバ側で引く。
細かい数値は眺めるだけで大丈夫です。
「一部ユーザーだけ落ちる + ログが無い = トークン肥大化を疑う」 だけ覚えて帰ってください。
JWT が向かない場面
| やりたいこと | JWT との相性 | 理由 |
|---|---|---|
| 退職・アカウント停止を即座に反映したい | 向かない | 発行済みの券は期限まで有効。回収できない |
| 権限がリクエストごとに頻繁に変わる | 向かない | 券は発行した瞬間のスナップショット |
| セッション状態を持たせたい | 向かない | サーバ側セッションのほうが単純 |
| 発行者と検証者が別システム | 向く | 問い合わせ不要でその場で判断できる |
| 検証を大規模にスケールさせたい | 向く | 検証にデータベースアクセスが要らない |
冒頭の話に戻ります。
「その場で判断できる」利点と、「発行後に取り消せない」欠点は、同じ性質から出ています。
片方だけを選ぶことはできません。
ここを理解していれば、「JWT を使うべきか」は自分で判断できます。
よくある誤解
| よくある誤解 | 実際は |
|---|---|
| 「JWT は暗号化されている」 | 署名が付いているだけ。中身は誰でも読めます |
| 「デコードできた=検証できた」 | デコードは誰でもできます。照合しなければ何の保証もありません |
| 「HS256 のほうが軽くて安全そう」 | 検証者全員が発行者にもなれる方式。配る先が増えるほど危険です |
「JWT の Header には必ず typ がある」 |
Cognito は kid と alg の2つだけです。用途の判定は token_use で行います |
まとめ
| 観点 | 押さえるポイント |
|---|---|
| 構造 | Header / Payload / Signature の3つをドットでつないだ文字列 |
| エンコード | Base64URL はエンコードであって暗号化ではない。中身は読めるので、秘密は載せない |
| 署名対象 | ハンコは「受け取った文字列そのもの」に押されている。組み立て直すと必ず落ちる |
| 署名方式 | HS256 は合鍵、RS256 は実印と印鑑証明。IdP が RS256 を使うのは秘密を配らずに済むから |
| 検証の順番 | 方式を固定 → 鍵を選ぶ → 署名を照合 → クレームを確認 |
| 切り分け | 「全員か・一部か」「常時か・散発か」の2軸で当たりを付ける |
| サイズ / 取り扱い | 太ると「一部ユーザーだけ落ちてログが残らない」障害に。ログに出さない・外部サイトに貼らない・短命にするまでがセット |
2原則だけ持ち帰るなら、alg を信じない・照合を省かない です。
次回予告
第5回「ID Token・Access Token・Refresh Tokenの違い」。
前回: 第3回「OpenID Connect(OIDC)とは?OAuthとの違いを図解で理解する」
この記事では「JWT の中身」を分解しましたが、実は ID トークンもアクセストークンも見た目は同じ JWT です。
第5回では、同じ形をした3種類の券がなぜ役割ごとに分かれているのか、どれをどこに送るべきなのかを整理します。
参考リンク
- RFC 7519 JSON Web Token(全体像、時刻は UNIX 秒)https://www.rfc-editor.org/rfc/rfc7519.html
- RFC 7515 JSON Web Signature(§2 パディングを落とし改行を入れない定義、署名対象=受け取った Base64URL 文字列、§10.9 タイミング攻撃)https://www.rfc-editor.org/rfc/rfc7515.html
- RFC 7517 JSON Web Key(JWKS の構造と
kid)https://www.rfc-editor.org/rfc/rfc7517.html - RFC 7518 JSON Web Algorithms(
algの一覧と実装要求レベル)https://www.rfc-editor.org/rfc/rfc7518.html - RFC 7165 JOSE のユースケースと要件(JSON に正準形が無いこと、XML 署名への反省)https://www.rfc-editor.org/rfc/rfc7165.html
- RFC 8725 / BCP 225 JWT Best Current Practices(許可アルゴリズムの固定、
typによる取り違え防止)https://www.rfc-editor.org/rfc/rfc8725.html - RFC 4648 Base64 エンコーディング(§5 URL 安全な文字集合、§3.2 パディングは原則必須)https://www.rfc-editor.org/rfc/rfc4648.html
- RFC 6750 OAuth 2.0 Bearer Token Usage(
invalid_tokenと 401 の形式)https://www.rfc-editor.org/rfc/rfc6750.html - Amazon Cognito JWT の検証(
algはRS256、token_useの確認)
https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html - Amazon Cognito ID トークンの理解(Header は
kidとalgのみ)
https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-id-token.html - API Gateway HTTP API のクォータ(ヘッダー合計 10240 バイト)
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-quotas.html - ALB トラブルシューティング(
400/413、JWT エラー理由コード、562 JWKS Request Failed)
https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html - Amazon Cognito クォータ(カスタム属性は 50 個)
https://docs.aws.amazon.com/cognito/latest/developerguide/quotas.html


