JWTの解剖:ヘッダー、ペイロード、署名、base64url、そしてデコードと検証の違い
3つのドット:JSON Web Tokenの構造
JSON Web Tokenは、3つのbase64urlエンコードされた値をドットで区切った文字列です:base64url(ヘッダー).base64url(ペイロード).base64url(署名)。RFC 7519は2015年5月に公開され、このフォーマットを定義しています。JWTがAuthorizationヘッダー、Cookie、URLクエリパラメータのいずれに現れても、この構造に従います。3つの部分はそれぞれ独立してデコードでき、署名が暗号的な保証でそれらを結びつけています。
このフォーマットはコンパクトでURLセーフなように設計されました。JWTが登場する以前、API認証はサーバー側に格納された不透明なセッション識別子に依存することが多く、リクエストごとにデータベース検索が必要でした。JWTはクレームをトークン自体に直接埋め込むため、ステートレスなサーバーが検索なしに検証できます。この利便性には責任が伴います:サーバーが即座に無効化できるセッショントークンと異なり、署名済みJWTはサーバーが拒否リストを管理しない限り、expクレームが失効するまで有効のままです。
base64urlエンコーディング:base64のURLセーフな変形
標準base64はバイナリデータを64文字でエンコードします:A–Z、a–z、0–9、+、/で、=でパディングします。このうち2文字(+と/)はURLやHTTPクエリ文字列で特別な意味を持ちます。RFC 4648 セクション5で定義されたbase64urlは、+を-に、/を_に置き換え、末尾の=パディングを完全に省略します。結果として得られる文字列は、追加のエンコードなしにURL、HTTPヘッダー、Cookieで直接使用できます。
膨張率は標準base64と同じで、入力3バイトが4文字のbase64urlになります——4/3倍、約33%の増加です。エンコードはキーなしで逆算可能なため、JWTを入手した人は誰でもヘッダーとペイロードをデコードして読み取れます——base64urlはエンコーディングであり、暗号化ではありません。
ヘッダー:アルゴリズムの選択とトークンタイプ
ヘッダーは受信者にトークンの処理方法を伝えるJSONオブジェクトです。最小構成のヘッダーには2つのフィールドがあります:alg(署名アルゴリズム)とtyp(標準トークンでは常に"JWT")。RFC 7518がアルゴリズム識別子を定義しています。最も一般的なものは:HS256——HMAC-SHA256、署名と検証に同じ秘密鍵を使う対称アルゴリズム;RS256——SHA-256付きRSA-PKCS1v1.5、非対称で秘密鍵で署名し公開鍵で検証;ES256——P-256とSHA-256を使うECDSA、非対称でRSAより短い署名。
対称アルゴリズムと非対称アルゴリズムの選択は実際のアーキテクチャに影響します。HS256では、トークンを検証する必要があるすべてのサービスが共有秘密を持つ必要があります。RS256やES256では、発行者は秘密鍵を秘密に保ちながら公開鍵のみを公開し(JWKSエンドポイント経由が一般的)、検証サービスは署名素材を扱いません。マルチサービスやサードパーティ統合では非対称アルゴリズムが推奨されます。
ペイロード:登録クレームとその意味
ペイロードはクレームを含むJSONオブジェクトです——エンティティ(通常は認証済みユーザー)に関する主張と、トークン自体のメタデータです。RFC 7519 セクション4.1は、意味が標準化された7つの登録クレーム名を定義しています:iss(発行者)、sub(主題、通常はユーザーID)、aud(対象者、意図された受信者)、exp(有効期限、これを過ぎたNumericDate後はトークンを受け入れてはならない)、nbf(有効開始時刻、このNumericDate前はトークンを受け入れてはならない)、iat(発行時刻)、jti(JWT ID、リプレイ攻撃防止に役立つ一意識別子)。
NumericDateはUTC 1970-01-01T00:00:00Z(Unixエポック)からの秒数であり、ミリ秒ではありません。JavaScriptのDate.now()(ミリ秒を返す)に慣れた開発者がトークン検証コードを書く際の一般的なバグ源です。標準JWTのペイロードは署名されていますが暗号化されていません——トークンを傍受した誰もがすべてのクレームを読めます。ペイロードに機密データが含まれる場合はJWE(RFC 7516)で暗号化するか、TLSのみで送信してください。
署名:何を保護するか——とalg:noneの落とし穴
署名は署名入力に対して計算されます:base64url(ヘッダー) + "." + base64url(ペイロード)。HS256の場合、署名はHMAC-SHA256(秘密鍵, 署名入力)をbase64urlエンコードしたものです。署名入力にはヘッダーとペイロードの両方が含まれるため、どちらかのわずかな変更——1文字でも——で署名は無効になります。署名を正しく検証したサーバーは、トークンが発行後に改ざんされていないことを確信できます。
2015年、複数のJWTライブラリで広く知られた脆弱性クラスが発見されました:alg:none攻撃。RFC 7519は技術的に"alg":"none"を署名なしの未保護JWTを示すものとして許可しています。欠陥:一部のライブラリは(未検証の)ヘッダーからalgフィールドを読み取って検証パスを選択していました——攻撃者がヘッダーを"alg":"none"に変更して署名を削除すると、ライブラリは署名をまったく確認せずにトークンを有効と宣言しました。CVE-2015-9235は人気のnode-jsonwebtokenライブラリにおけるこの問題を記録しています。修正策:検証側は期待するアルゴリズムを明示的に指定し、ヘッダーが異なるアルゴリズムを指定するトークンはすべて拒否しなければなりません。
デコードと検証:この違いが重要な理由
JWTをデコードするとは、3つの部分にbase64url復号を適用して、JSONヘッダー、ペイロード、生の署名バイトを取り出すことです。鍵は不要です。JWT文字列を持つ誰もがデコードしてクレームを読めます。これは意図的な設計です——JWTは不透明なコンテナではありません。認証問題をデバッグする開発者、アクセスログを読む運用エンジニア、システムを監査するセキュリティ研究者:誰でも持っているJWTをデコードできます。これこそJWTデコーダーツールが役立つ理由です。
JWTを検証するとは、正確なヘッダーとペイロードのバイトに対して正しい鍵で署名が生成されたことを確認することです。検証が成功すると2つのことが証明されます:署名鍵の保有者がトークンを作成したこと(真正性)、署名以降ヘッダーもペイロードも変更されていないこと(完全性)。検証には鍵が必要です——HMACアルゴリズムには共有秘密、RSA/ECDSAには公開鍵。
重大なミスは、検証なしにデコードされたJWTを信頼することです。localStorageのJWTからクレームを読み取り、サーバー側の署名検証なしにそれを信頼するクライアントは、誰でも自由に偽造できるデータを信頼しています。攻撃者は{"sub":"admin","role":"superuser"}を作り、任意のヘッダーでbase64urlエンコードし、偽の署名や空の署名を添付できます——それは問題なくデコードされます。認可の決定は常にサーバー側の暗号学的検証の後に行わなければなりません。 JWTデコーダーツールは検査とデバッグに適したツールですが、実稼働アプリケーションのサーバー側検証の代替にはなりません。