はじめに:JSON Web Tokenとは何か
JSON Web Token(JWT)は、RFC 7519 として標準化されたオープン規格で、JSON オブジェクトとして当事者間でセキュアに情報を伝達するための、コンパクトで自己完結型の方法を定義しています。デジタル署名されているため、その情報は検証・信頼が可能です。JWT は秘密鍵(HMAC アルゴリズム)または公開鍵/秘密鍵のペア(RSA または ECDSA)を使って署名されます。
JWT は分散システムにおける共通の課題を解決するために生まれました。「サーバーはデータベースでセッションを検索せずに、クライアントからのリクエストをどのように信頼できるか?」という問いに対する答えが、ユーザーの ID と権限をトークンに直接エンコードして署名し、クライアントがすべてのリクエストにそれを添付して送るという方式です。
現在、JWT はモダンな認証・認可の基盤技術として広く活用されています:
- シングルサインオン(SSO): 一度のログインで複数のサービスにアクセス可能。
- API 認証: モバイルアプリや SPA が Bearer トークンで API 呼び出しを認証。
- OAuth 2.0 / OpenID Connect: アクセストークンや ID トークンとして JWT を使用。
- マイクロサービス: 中央セッションストアなしにサービス間で呼び出し元の身元を検証。
JWT の構造:header.payload.signature
JWT は、ドット(.)で区切られた 3 つの Base64url エンコード部分からなる文字列です:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
第1部 — Header(ヘッダー) をデコードすると:
{
"alg": "HS256",
"typ": "JWT"
}
ヘッダーは署名アルゴリズム(alg)とトークンタイプ(typ)を指定します。
第2部 — Payload(ペイロード) をデコードすると:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
ペイロードには**クレーム(Claims)**が含まれています。クレームはエンティティ(通常はユーザー)についての記述とメタデータです。
第3部 — Signature(署名)(HS256 の場合)は以下のように計算されます:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
署名はトークンが改ざんされていないことを保証します。重要: ペイロードは Base64url エンコードされているだけで、暗号化されていません。JWT を入手した人は誰でもすべてのクレームを読み取ることができます。
署名アルゴリズム:HS256、RS256、ES256
HS256(HMAC-SHA256)— 対称アルゴリズム
署名と検証に同一の共有シークレットを使用します。実装がシンプルですが、発行者と検証者の両方がそのシークレットを知っている必要があります。
- 適用場面: 単体アプリケーション、またはシークレットを共有する密結合サービス。
- リスク: シークレットが漏洩した場合、すべてのトークンが偽造可能になります。
RS256(RSA-SHA256)— 非対称アルゴリズム
秘密鍵で署名し、公開鍵で検証します。秘密鍵は秘密に保ち、公開鍵は JWKS エンドポイントなどで自由に公開できます。
- 適用場面: 分散システム、マイクロサービス、複数サービスが検証する場合。
- 鍵長: 最低 2048 ビットの RSA 鍵を推奨。
ES256(ECDSA-SHA256)— 非対称アルゴリズム
P-256 曲線を使った楕円曲線デジタル署名アルゴリズムを使用します。RS256 と同等のセキュリティで、より小さな署名を生成します。
- 適用場面: パフォーマンスが重要な環境や帯域幅が制限された環境。
| アルゴリズム | 種別 | 鍵 | 署名サイズ | 適用場面 |
|---|---|---|---|---|
| HS256 | 対称 | 共有シークレット | 32 バイト | 単一サービスアプリ |
| RS256 | 非対称 | RSA 鍵ペア | 256+ バイト | 分散システム |
| ES256 | 非対称 | EC 鍵ペア | 64 バイト | 高パフォーマンス API |
JWT クレーム:登録済み・公開・プライベート
クレームはペイロードにエンコードされた、エンティティ(通常はユーザー)についての記述とメタデータです。
登録済みクレーム(IANA 定義)
| クレーム | 正式名称 | 説明 |
|---|---|---|
iss |
Issuer | トークンを発行したエンティティ(例:"auth.example.com") |
sub |
Subject | トークンが指すエンティティ(例:ユーザー ID) |
aud |
Audience | トークンの対象受信者(サービス識別子) |
exp |
Expiration | トークンが無効になる Unix タイムスタンプ |
nbf |
Not Before | このタイムスタンプ以前はトークンを使用してはならない |
iat |
Issued At | トークンが発行された Unix タイムスタンプ |
jti |
JWT ID | リプレイ攻撃防止のための一意識別子 |
公開クレーム
IANA JWT Claims Registry に登録されたクレーム。例:email、name、picture、roles。
プライベートクレーム
発行者と消費者が独自に取り決めたカスタムクレーム。衝突を避けるため名前空間を使用することを推奨します:
{
"https://example.com/tenant_id": "acme-corp",
"https://example.com/roles": ["admin", "editor"]
}
JWT 認証フローの仕組み
- ユーザーログイン: クライアントが認証サーバーに認証情報(ユーザー名 + パスワード)を送信。
- トークン発行: サーバーが認証情報を検証し、適切なクレームを含む JWT を作成・署名してクライアントに返却。
- トークン保存: クライアントが JWT をメモリ、localStorage、または HttpOnly Cookie に保存。
- 認証済みリクエスト: 保護されたリソースへのリクエストごとに、
Authorizationヘッダーに JWT を添付:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - トークン検証: サーバーがトークンを取り出し、署名を検証し、
expをチェックして、クレームに基づきアクセスを許可または拒否。 - トークンリフレッシュ: アクセストークンが期限切れになったら、クライアントは長期有効なリフレッシュトークンを使って再認証なしで新しいアクセストークンを取得。
クライアント 認証サーバー リソースサーバー
| | |
|-- POST /login ---->| |
|<-- 200 + JWT ------| |
| | |
|-- GET /api (Authorization: Bearer JWT) ->|
| | JWT を検証 |
|<----------- 200 + データ ---------------->|
セキュリティ上の考慮事項
ペイロードは暗号化されていない
Base64url エンコードは暗号化ではありません。 JWT を持っている人は誰でも、すぐにすべてのクレームをデコードして読み取ることができます。JWT ペイロードには絶対に含めてはいけないもの:
- パスワードやパスワードハッシュ
- クレジットカード番号や金融データ
- 必要最低限を超える個人情報(PII)
- 秘密鍵や内部シークレット
必ず署名を検証する
// Node.js — jsonwebtoken ライブラリ
const jwt = require("jsonwebtoken");
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.sub); // 信頼できるユーザー ID
} catch (err) {
res.status(401).json({ error: "無効または期限切れのトークンです" });
}
短い有効期限を設定する
アクセストークンの有効期限は短くすること(15 分〜1 時間)。長期セッションにはリフレッシュトークンを使用し、トークン盗難時の被害を最小化します。
必ず HTTPS を使用する
暗号化されていない通信でトークンを送信してはいけません。傍受されたトークンは、盗まれたパスワードと同じです。
一般的な脆弱性
1. アルゴリズム混同攻撃(CVE-2015-9235)
攻撃者がヘッダーの alg フィールドを RS256 から HS256 に変更し、サーバーの公開鍵を HMAC シークレットとして使ってトークンに署名します。不適切な実装のサーバーは、これを受け入れてしまいます。
対策: サーバー側で期待するアルゴリズムを明示的に指定します:
jwt.verify(token, publicKey, { algorithms: ["RS256"] });
2. "none" アルゴリズム攻撃
サーバーが alg: "none" を受け入れる場合、攻撃者は署名なしのトークンを偽造できます。
対策: alg: "none" のトークンは明示的に拒否します。最新のライブラリはこの問題を修正していますが、古いバージョンは脆弱な場合があります。
3. 弱いシークレット(HS256)
短いまたは推測しやすい HMAC シークレットは、hashcat などのツールを使って取得したトークンからオフラインでブルートフォース攻撃される可能性があります。
対策: 最低 256 ビットの暗号論的乱数シークレットを使用します:
openssl rand -hex 32
4. クレーム検証の欠如
iss、aud、nbf を検証しないと、クロスサービスのトークン流用や、有効期間前のトークン使用を許してしまいます。
5. 安全でないトークン保存
localStorage はページ上のあらゆる JavaScript からアクセス可能で、XSS 攻撃に対してトークンが脆弱になります。センシティブなトークンには HttpOnly Cookie を使用することを推奨します。
JWT vs セッショントークン:ステートレス vs ステートフル
| 特性 | JWT(ステートレス) | セッショントークン(ステートフル) |
|---|---|---|
| 保存場所 | クライアント側 | サーバー側(DB またはキャッシュ) |
| スケーラビリティ | 優秀 — サーバー検索不要 | 共有セッションストアが必要 |
| 失効 | 困難 — exp まで有効 |
簡単 — ストアから削除するだけ |
| トークンサイズ | リクエストごとに大きめ | 小さいトークン、データはサーバー側 |
| マイクロサービス | 理想的 — 各サービスが独立して検証 | 共有セッションインフラが必要 |
| ペイロード可視性 | トークンを持つ人は誰でも読める | データはサーバー側にのみ保存 |
JWT を選ぶ場面: 分散システム、ステートレス REST API、モバイルアプリの構築時。 セッションを選ぶ場面: 即時失効が必要な場合(例:「すべての端末からログアウト」)。
失効の課題
JWT は自己完結型のため、有効期限前にトークンをキャンセルするには追加インフラが必要です:
- 短い有効期限 + リフレッシュトークン: アクセストークンを短命に保ち、リフレッシュトークンを DB で失効させる。
- トークンブロックリスト: 失効した
jtiを Redis などの高速ストアで管理(一部ステートフル性を再導入)。 - シークレットローテーション: 署名シークレットをローテーションしてすべてのトークンを無効化(全ユーザーに影響)。
OAuth 2.0 と OpenID Connect
OAuth 2.0
OAuth 2.0 は認可フレームワークであり、特定のトークン形式を義務付けていません。しかし JWT は自己完結型で認可スコープを持つことができるため、OAuth アクセストークンとして広く使われています:
{
"iss": "https://auth.example.com",
"sub": "user_123",
"aud": "https://api.example.com",
"scope": "read:profile write:posts",
"exp": 1893456000
}
OpenID Connect(OIDC)
OpenID Connect は OAuth 2.0 の上に構築されたアイデンティティレイヤーで、ID Token を導入しています。ID Token は常に JWT であり、認証されたユーザーのアイデンティティクレームを含みます:
{
"iss": "https://accounts.google.com",
"sub": "110169484474386276334",
"aud": "my-client-id.apps.googleusercontent.com",
"email": "[email protected]",
"name": "Jane Doe",
"exp": 1893456000,
"iat": 1893452400
}
OIDC は JWKS エンドポイント(/.well-known/jwks.json)も定義しており、認可サーバーがここに公開鍵を公開します。どのサービスも事前の鍵交換なしにこれらの鍵を取得してトークンを検証できます。
ベストプラクティス
- 公開向けシステムには非対称アルゴリズム(RS256/ES256)を使用 — 署名秘密鍵を保護しながら、誰でも公開鍵で検証できるようにします。
- アクセストークンには短い
expを設定 — 15 分が一般的で安全なデフォルト値です。 - リフレッシュトークンは必ずローテーション — 使用するたびに新しいトークンを発行し、古いものを無効化します。
- すべての関連クレームを検証 — 最低でも
iss、aud、exp、nbfを確認します。 - 期待するアルゴリズムを明示的に指定 — トークンヘッダーの
algフィールドだけに頼ってはいけません。 - センシティブなデータをペイロードに入れない — エンコードであり、暗号化ではありません。
- 常に HTTPS を使用 — 暗号化されていない通信でトークンを送信しないこと。
- トークンを安全に保存 — localStorage より
HttpOnly、Secure、SameSite=StrictCookie を優先します。 - トークン失効戦略を実装 — ブロックリストか、短命トークン + リフレッシュローテーションを使用します。
- JWT ライブラリを最新に保つ — 新バージョンでセキュリティ脆弱性が修正されます。
よくある質問(FAQ)
Q: JWT ペイロードを復号化できますか? A: JWT ペイロードは Base64url エンコードされているだけで、暗号化されていません。鍵がなくても誰でも読めます。機密性が必要な場合は、ペイロードを暗号化する JWE(JSON Web Encryption、RFC 7516)を使用してください。
Q: decode と verify の違いは何ですか?
A: decode は各部分を Base64url デコードして JSON を返すだけで、セキュリティチェックは行いません。verify は署名を鍵で検証し、exp などの時間系クレームもチェックします。本番環境では必ず verify を使用してください。
Q: トークンのリフレッシュを安全に扱うには? A: 短命なアクセストークン(15〜60 分)と長命なリフレッシュトークン(数日〜数週間)を発行します。リフレッシュトークンはサーバー側で保存して失効できるようにします。アクセストークンが期限切れになったら、クライアントは専用エンドポイントにリフレッシュトークンを送り、新しいアクセストークンを取得します。
Q: JWT は認証と認可の両方に使えますか?
A: はい。同じトークンにアイデンティティクレーム(誰であるか)と認可クレーム(何ができるか、例:roles、scope)を含めることができます。トークンサイズは適切に保ちましょう。
Q: JWT シークレットが漏洩した場合はどうすればよいですか? A: 攻撃者は任意のユーザーのトークンを偽造できます。直ちにシークレットをローテーション(すべての既存トークンが無効になります)し、漏洩原因を調査して、すべてのユーザーに再認証を要求してください。
Q: HS256 と RS256 のどちらを使うべきですか? A: 複数のサービスがトークンを検証する場合や、検証側が完全に信頼できない場合は RS256(または ES256)を使用します。同じサービスが発行と検証を行い、シークレットを厳密に管理できる場合のみ HS256 を使用してください。
Q: JWT の最大サイズはどのくらいですか? A: HTTP ヘッダーサイズ制限とパフォーマンス問題を避けるため、4 KB 未満に収めることを推奨します。大きなデータ構造をペイロードに埋め込まず、ID で参照するようにしましょう。