トークンベース認証の代表格であるJWT(JSON Web Token)は、現代のWeb開発において欠かせない技術です。前回の記事で認証と認可の基礎を学びましたが、今回はJWTの内部構造に踏み込み、トークンがどのように生成され、検証されるのかを詳しく解説します。
JWTは一見すると意味不明な文字列に見えますが、その構造を理解すれば、トークンの中身を読み解き、セキュリティ上の問題点を発見できるようになります。この記事を読み終えるころには、JWTの構造を正確に説明でき、開発者ツールやコマンドラインでトークンを確認・検証できるスキルが身についているでしょう。
JWTとは何か
JWT(JSON Web Token、発音は「ジョット」)は、RFC 7519で標準化されたトークン形式です。2者間で情報を安全にやり取りするための、コンパクトでURLセーフな手段を提供します。
JWTの特徴
JWTには以下の特徴があります。
- 自己完結型(Self-contained): トークン自体に必要な情報がすべて含まれているため、サーバー側でセッション情報を保持する必要がありません
- コンパクト: Base64URLエンコードされた形式で、HTTPヘッダーやURLパラメータに含めやすいサイズです
- 署名付き: デジタル署名により、トークンが改ざんされていないことを検証できます
- 標準化: RFC 7519として標準化されており、多くのプログラミング言語でライブラリが提供されています
JWTが利用される場面
JWTは主に以下のシーンで活用されています。
- 認証(Authentication): ログイン後にJWTを発行し、以降のリクエストでユーザーを識別します
- 認可(Authorization): トークンに権限情報を含め、APIへのアクセス制御に使用します
- 情報交換: 署名によって送信者の正当性を確認しながら、システム間で安全に情報をやり取りします
JWTの構造を理解する
JWTは3つのパートから構成され、それぞれがピリオド(.)で区切られています。
|
|
具体的なJWTの例を見てみましょう。
|
|
この一見ランダムな文字列は、以下の3つのパートで構成されています。
flowchart LR
subgraph JWT["JWT構造"]
direction LR
H["Header<br>eyJhbGci..."]
P["Payload<br>eyJzdWIi..."]
S["Signature<br>SflKxwRJ..."]
end
H --- dot1["."] --- P --- dot2["."] --- S| パート | 内容 | 例 |
|---|---|---|
| Header | アルゴリズムとトークンタイプ | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 |
| Payload | クレーム情報 | eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZ... |
| Signature | 署名データ | SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |
JWTの3つのパートを図解
flowchart TD
subgraph JWT["完成したJWT"]
direction TB
A["Header<br>アルゴリズム<br>トークンタイプ"]
B["Payload<br>クレーム情報<br>ユーザー情報<br>有効期限など"]
C["Signature<br>改ざん検知用<br>署名データ"]
end
A -->|Base64URLエンコード| D[".で連結"]
B -->|Base64URLエンコード| D
C -->|Base64URLエンコード| D
D --> E["xxxxx.yyyyy.zzzzz"]それでは、各パートの詳細を見ていきましょう。
Header(ヘッダー)の構造
Headerはトークンのメタ情報を格納する部分です。JSONオブジェクトをBase64URLエンコードして生成されます。
Headerの構成要素
Headerには通常、以下の2つの情報が含まれます。
|
|
| フィールド | 説明 | 例 |
|---|---|---|
alg |
署名に使用するアルゴリズム | HS256, RS256, ES256 |
typ |
トークンのタイプ | JWT |
署名アルゴリズムの種類
JWTで使用される主要な署名アルゴリズムは以下の通りです。
| アルゴリズム | 種類 | 説明 |
|---|---|---|
| HS256 | 共通鍵暗号 | HMAC + SHA-256。送信者と受信者が同じ秘密鍵を共有 |
| HS384 | 共通鍵暗号 | HMAC + SHA-384 |
| HS512 | 共通鍵暗号 | HMAC + SHA-512 |
| RS256 | 公開鍵暗号 | RSA + SHA-256。秘密鍵で署名し、公開鍵で検証 |
| RS384 | 公開鍵暗号 | RSA + SHA-384 |
| RS512 | 公開鍵暗号 | RSA + SHA-512 |
| ES256 | 公開鍵暗号 | ECDSA + P-256曲線 + SHA-256 |
| ES384 | 公開鍵暗号 | ECDSA + P-384曲線 + SHA-384 |
| ES512 | 公開鍵暗号 | ECDSA + P-521曲線 + SHA-512 |
| none | なし | 署名なし(セキュリティ上非推奨) |
HeaderのBase64URLエンコード
上記のJSONをBase64URLエンコードすると、以下の文字列になります。
|
|
Base64URLエンコードは、通常のBase64エンコードの出力から+を-に、/を_に置換し、パディングの=を除去したものです。これによりURLセーフな文字列が得られます。
Payload(ペイロード)の構造
Payloadはトークンの本体であり、「クレーム(Claims)」と呼ばれる情報を格納します。クレームとは、エンティティ(通常はユーザー)に関する情報や、トークン自体に関するメタデータです。
クレームの種類
クレームは3つのカテゴリに分類されます。
1. 登録済みクレーム(Registered Claims)
RFC 7519で事前に定義されたクレームです。使用は任意ですが、相互運用性を高めるために推奨されます。
| クレーム | 名前 | 説明 |
|---|---|---|
iss |
Issuer | トークンの発行者 |
sub |
Subject | トークンの主題(通常はユーザーID) |
aud |
Audience | トークンの受信者(対象者) |
exp |
Expiration Time | トークンの有効期限(UNIXタイムスタンプ) |
nbf |
Not Before | トークンが有効になる時刻 |
iat |
Issued At | トークンの発行時刻 |
jti |
JWT ID | トークンの一意な識別子 |
クレーム名が3文字と短いのは、JWTをできるだけコンパクトに保つためです。
2. パブリッククレーム(Public Claims)
IANA JSON Web Token Claimsレジストリに登録されたクレーム、またはURIを含む衝突耐性のある名前で定義されたクレームです。
|
|
3. プライベートクレーム(Private Claims)
アプリケーション固有のカスタムクレームです。送信者と受信者の間で合意された独自の情報を格納します。
|
|
Payloadの例
実際のPayloadは以下のようになります。
|
|
このJSONをBase64URLエンコードすると、JWTの2番目のパートになります。
Payloadに関する重要な注意点
署名付きJWTであっても、Payloadの内容は誰でも読むことができます。Base64URLエンコードは暗号化ではなく、単なるエンコードです。そのため、パスワードやクレジットカード番号などの機密情報をPayloadに含めてはいけません。
警告: JWTのPayloadに機密情報を格納しないでください
格納可否 例 NG パスワード、クレジットカード番号、個人情報 OK ユーザーID、権限情報、トークンのメタデータ
Signature(署名)の構造
Signatureは、トークンが改ざんされていないことを保証するための部分です。Header、Payload、そして秘密鍵(またはキーペア)を使用して生成されます。
署名の生成プロセス
署名は以下の手順で生成されます。
- HeaderをBase64URLエンコード
- PayloadをBase64URLエンコード
- エンコードしたHeaderとPayloadをピリオドで連結
- 連結した文字列を、指定されたアルゴリズムと秘密鍵で署名
- 署名結果をBase64URLエンコード
HMAC SHA-256アルゴリズム(HS256)の場合、署名は以下の計算で生成されます。
|
|
署名生成の図解
flowchart TD
subgraph Input["入力"]
H["Header<br>{alg:HS256, typ:JWT}"]
P["Payload<br>{sub:123, name:John}"]
end
H -->|Base64URLEncode| HE["eyJhbGci..."]
P -->|Base64URLEncode| PE["eyJzdWIi..."]
HE --> C["eyJhbGci... . eyJzdWIi..."]
PE --> C
C --> HMAC["HMACSHA256<br>data + secret_key"]
HMAC --> BIN["署名結果<br>バイナリ"]
BIN -->|Base64URLEncode| SIG["SflKxwRJSMeKKF2QT4..."]共通鍵方式と公開鍵方式の違い
署名方式には大きく分けて2つのアプローチがあります。
共通鍵方式(HS256など)
flowchart LR
subgraph Issuer["発行側"]
IS["秘密鍵で署名"]
end
subgraph Verifier["検証側"]
VE["秘密鍵で検証"]
end
Issuer <-->|同じ秘密鍵を共有| Verifier公開鍵方式(RS256など)
flowchart LR
subgraph Issuer["発行側"]
IS["秘密鍵で署名"]
end
subgraph Verifier["検証側"]
VE["公開鍵で検証"]
end
Issuer -->|公開鍵を配布| Verifier公開鍵方式は、秘密鍵を検証側と共有する必要がないため、マイクロサービスアーキテクチャやサードパーティとの連携に適しています。
JWTのエンコードと署名の仕組み
ここまでの内容を踏まえ、JWTが生成される全体のプロセスを確認しましょう。
JWTエンコードの完全なフロー
入力データ
| 項目 | 値 |
|---|---|
| Header JSON | {"alg":"HS256","typ":"JWT"} |
| Payload JSON | {"sub":"1234567890","name":"John Doe","iat":1516239022} |
| Secret Key | your-256-bit-secret |
処理フロー
flowchart TD
subgraph Step1["Step 1: Header エンコード"]
H1["{alg:HS256, typ:JWT}"] -->|Base64URL| H2["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"]
end
subgraph Step2["Step 2: Payload エンコード"]
P1["{sub:1234567890, name:John Doe, ...}"] -->|Base64URL| P2["eyJzdWIiOiIxMjM0NTY3ODkwIi..."]
end
subgraph Step3["Step 3: 署名対象作成"]
H2 --> CONCAT["Header.Payload"]
P2 --> CONCAT
end
subgraph Step4["Step 4: 署名生成"]
CONCAT -->|HMAC-SHA256| SIG["SflKxwRJSMeKKF2QT4..."]
end
subgraph Step5["Step 5: JWT完成"]
SIG --> JWT["Header.Payload.Signature"]
end出力
|
|
Base64URLエンコードの仕組み
Base64URLエンコードは、バイナリデータを安全にテキストに変換する方法です。
| 項目 | Base64 | Base64URL |
|---|---|---|
| 使用文字 | A-Z, a-z, 0-9, +, / | A-Z, a-z, 0-9, -, _ |
| パディング | = | なし |
| 変換例 | Hello+World/Test== |
Hello-World_Test |
この変換により、JWTはURLのクエリパラメータやHTTPヘッダーに安全に含めることができます。
JWTの検証プロセス
JWTを受け取った側は、トークンが有効かどうかを検証する必要があります。検証プロセスは複数のステップで構成されます。
検証の全体フロー
flowchart TD
START["JWTを受信"] --> STEP1
subgraph STEP1["Step 1: 構造の検証"]
S1A["ピリオドで3つに分割可能か"]
S1B["各パートが空でないか"]
end
STEP1 --> STEP2
subgraph STEP2["Step 2: Headerのデコード"]
S2A["Base64URLデコード"]
S2B["有効なJSONか"]
S2C["algフィールドが存在するか"]
end
STEP2 --> STEP3
subgraph STEP3["Step 3: 署名の検証"]
S3A["Header.Payloadを再計算"]
S3B["秘密鍵/公開鍵で署名検証"]
S3C["署名が一致するか"]
end
STEP3 --> STEP4
subgraph STEP4["Step 4: クレームの検証"]
S4A["exp: 有効期限チェック"]
S4B["nbf: 有効開始時刻チェック"]
S4C["iss: 発行者チェック"]
S4D["aud: 対象者チェック"]
end
STEP4 --> END["検証完了<br>トークン有効/無効"]署名検証の詳細
署名検証は、トークンが改ざんされていないことを確認する最も重要なステップです。
受信したJWT: header.payload.signature
検証手順
- headerとpayloadを取り出す
- header + “.” + payload で署名対象を再構成
- 同じアルゴリズムと鍵で署名を再計算
- 再計算した署名と受信した署名を比較
flowchart TD
R["受信した署名"] --> CMP{"比較"}
C["再計算した署名"] --> CMP
CMP -->|一致| VALID["有効"]
CMP -->|不一致| INVALID["無効"]クレーム検証のポイント
署名が正しくても、クレームの検証が不十分だとセキュリティ上の問題が生じます。
必須の検証項目
| クレーム | 検証内容 | 備考 |
|---|---|---|
exp(有効期限) |
現在時刻 < exp であること | 多少のクロックスキュー(数分程度)は許容することが多い |
iat(発行時刻) |
現在時刻 > iat であること | 極端に未来の発行時刻は拒否 |
nbf(有効開始時刻) |
現在時刻 >= nbf であること | - |
iss(発行者) |
期待する発行者と一致すること | - |
aud(対象者) |
自身のサービスが対象者に含まれていること | - |
JWTの実際の利用例
JWTがどのように使われるか、具体的なシナリオを見てみましょう。
API認証でのJWT利用フロー
sequenceDiagram
participant U as ユーザー
participant A as 認証サーバー
participant API as APIサーバー
U->>A: 1. ログイン要求<br>(email, password)
A->>U: 2. 認証成功 → JWT発行
U->>API: 3. APIリクエスト + JWT<br>(Authorization: Bearer JWT)
Note over API: 4. JWT検証<br>- 署名検証<br>- クレーム検証
API->>U: 5. レスポンスHTTPリクエストでのJWT送信
JWTは通常、HTTPヘッダーのAuthorizationフィールドでBearerスキームを使用して送信します。
|
|
JWTを含むレスポンスの例
ログイン成功時に返されるレスポンスの例です。
|
|
開発者ツールでJWTを確認する方法
実際の開発では、JWTの内容を確認したり、デバッグしたりする場面が頻繁にあります。
ブラウザの開発者ツールで確認する
ChromeやFirefoxの開発者ツールを使用して、HTTPリクエストに含まれるJWTを確認できます。
- 開発者ツールを開く(F12またはCtrl+Shift+I)
- Networkタブを選択
- APIリクエストを選択
- HeadersセクションでAuthorizationヘッダーを確認
|
|
jwt.ioでJWTをデコードする
jwt.ioのデバッガーを使用すると、JWTの内容をWeb上で簡単に確認できます。
- https://jwt.io にアクセス
- Debuggerセクションにトークンを貼り付け
- Header、Payload、署名の検証結果が表示される
コマンドラインでJWTを確認する
bashとjqを使用して、ターミナルからJWTをデコードできます。
|
|
実行結果:
|
|
Node.jsでJWTを検証する
Node.jsのjsonwebtokenライブラリを使用した検証例です。
|
|
PythonでJWTを検証する
PythonのPyJWTライブラリを使用した検証例です。
|
|
JWTを使用する際の注意点
JWTを安全に使用するために、いくつかの重要な注意点があります。
セキュリティ上の注意点
| No | 注意点 | 詳細 |
|---|---|---|
| 1 | 機密情報をPayloadに含めない | Payloadは暗号化されておらず、Base64デコードで誰でも読める |
| 2 | 適切な有効期限を設定する | Access Tokenは短め(15分〜1時間程度)、Refresh Tokenで更新する仕組みを併用 |
| 3 | algヘッダーを検証時に信頼しない | サーバー側で許可するアルゴリズムを明示的に指定し、“none"アルゴリズムを拒否 |
| 4 | 秘密鍵を安全に管理する | 環境変数やシークレット管理サービスを使用し、ソースコードにハードコードしない |
| 5 | HTTPSを使用する | トークンが傍受されるリスクを軽減 |
“alg”: “none"攻撃への対策
署名アルゴリズムを「none」に変更する攻撃が知られています。
攻撃者が送信する悪意あるJWT
|
|
対策
| 対策 | 説明 |
|---|---|
| アルゴリズムの明示的なリスト化 | サーバー側で許可するアルゴリズムを指定 |
| “none"の拒否 | “none"アルゴリズムは常に拒否する |
| ライブラリ設定 | ライブラリの設定で無効化する |
Node.jsでの対策例:
|
|
まとめ
この記事では、JWTの構造と仕組みについて詳しく解説しました。
JWTの重要なポイントを振り返りましょう。
- JWTの構造: Header、Payload、Signatureの3つのパートで構成され、ピリオドで連結されます
- エンコード: 各パートはBase64URLエンコードされ、URLセーフな形式になります
- 署名の仕組み: HeaderとPayloadを連結し、秘密鍵で署名することで改ざんを検知できます
- 検証プロセス: 構造の検証、署名の検証、クレームの検証を順番に行います
- 利用シーン: API認証、認可、システム間の安全な情報交換に活用されます
JWTはステートレスな認証を実現する強力なツールですが、適切に使用しないとセキュリティリスクが生じます。次回の記事では、Access TokenとRefresh Tokenを組み合わせた認証フローの実装について解説します。