JWTトークン完全解説:構造・セキュリティ・よくある間違い
JSON Web Tokenの仕組み、内部構造、そして本番環境で開発者がはまりやすいセキュリティの落とし穴を理解しましょう。
JSON Web Token(JWT)はあらゆる場所で使われています。SPA、モバイルアプリ、マイクロサービスアーキテクチャの認証を世界中で支えています。しかし同時に、Web開発において最も誤解されやすいセキュリティの基本要素の一つでもあります。実装を誤れば、認証バイパス、権限昇格、アカウントの完全乗っ取りにつながりかねません。
JWTとは?
JWTはコンパクトでURL-safeな文字列で、ユーザーやセッションに関する主張であるクレームのセットをエンコードします。見た目はこのようになっています:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
ドットで区切られた3つのBase64URLエンコード済みセグメントで構成されています:
- Header — アルゴリズムとトークンタイプ
- Payload — クレーム(ユーザーデータ)
- Signature — 整合性を証明する暗号署名
任意のJWTをJWT Decoderに貼り付けると、外部サーバーに触れることなく3つのパートすべてを即座に確認できます。
JWTの構造
Header
{
"alg": "HS256",
"typ": "JWT"
}
algは署名アルゴリズムを指定します。代表的な値:
HS256— HMAC with SHA-256(対称、単一シークレット)RS256— RSA with SHA-256(非対称、公開鍵/秘密鍵)ES256— ECDSA with SHA-256(非対称、鍵サイズが小さい)
Payload
{
"sub": "user_abc123",
"email": "alice@example.com",
"role": "admin",
"iat": 1711670400,
"exp": 1711756800
}
標準の登録済みクレーム:
| クレーム | 意味 |
|---|---|
sub |
Subject(トークンが誰に関するものか) |
iss |
Issuer(トークンを発行したのは誰か) |
aud |
Audience(誰が受け入れるべきか) |
exp |
Expiration time(Unixタイムスタンプ) |
iat |
Issued at(Unixタイムスタンプ) |
nbf |
Not before(この時刻より前は受け入れない) |
Signature
HS256の場合:
HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
署名はトークンが改ざんされていないことを証明します。Payloadは暗号化されておらず、単にエンコードされているだけです。 誰でも読むことができます。
パスワードやクレジットカード番号などの機密データをJWT payloadに保存しないでください。
HS256とRS256:どちらを選ぶべきか?
HS256は単一の共有シークレットを使用します。トークンを検証する必要があるすべてのサービスが同じシークレットを持つ必要があります。モノリスには簡単ですが、マルチサービスアーキテクチャでは危険です。いずれかのサービスが侵害されると、攻撃者はトークンを偽造できます。
RS256は非対称鍵を使用します。認証サーバーが秘密鍵で署名し、他のすべてのサービスが公開鍵で検証します。コンシューマーサービスが侵害されても、攻撃者はトークンを偽造できません。複数のサービスを持つシステムではRS256を推奨します。
alg: noneの脆弱性
最も悪名高いJWT攻撃の一つです。一部の古いライブラリは"alg": "none"で署名なしのトークンを受け入れ、有効として扱っていました。攻撃者はこのように細工することができました:
{ "alg": "none" }
任意のpayloadを持たせて認証を完全にバイパスできます。
対策: JWTライブラリで許可するアルゴリズムを常に明示的に指定してください。noneは絶対に受け入れないでください。
// ❌ 危険
jwt.verify(token, secret);
// ✅ 安全 — HS256のみ受け入れる
jwt.verify(token, secret, { algorithms: ["HS256"] });
アルゴリズム混同攻撃
もう一つの重大な脆弱性:ライブラリがヘッダーからアルゴリズムを自動検出する場合、攻撃者はRS256をHS256に変更し、公開鍵をHMACシークレットとして(これは定義上、公開情報です)使ってトークンに署名できます。
対策: 期待するアルゴリズムを常にハードコードしてください。algヘッダーを信頼しないでください。
トークンの保存場所:JWTをどこに保管するか
| 保存場所 | XSSリスク | CSRFリスク | 備考 |
|---|---|---|---|
localStorage |
高 | なし | ページ上のすべてのJSからアクセス可能 |
sessionStorage |
高 | なし | タブを閉じると消去される |
| HTTP-only cookie | なし | 中 | Webアプリに最適;SameSite=Strictを使用 |
| メモリ(変数) | 低 | なし | リフレッシュ時に消える;SPAに向いている |
Webアプリケーションでは、SameSite=Strictを設定したHTTP-only cookieが最も安全な選択肢です。ネイティブアプリでは、セキュアなストレージAPI(Keychain、Keystore)が適切です。
有効期限とリフレッシュトークン
有効期限の短いアクセストークン(5〜15分)と有効期限の長いリフレッシュトークンを組み合わせるのが標準的なパターンです:
- ユーザーがログイン → サーバーがアクセストークン(15分)+リフレッシュトークン(7日間、DBに保存)を発行
- クライアントがアクセストークンをAPIコールに使用
- アクセストークンの有効期限が切れたら、クライアントがリフレッシュトークンを送信 → 新しいアクセストークンを取得
- ログアウト時にデータベースのリフレッシュトークンを無効化
これにより、アクセストークンが盗まれた場合の被害範囲を限定できます。
JWTの失効処理
JWTはステートレスです。一度発行されると、有効期限が切れるまで有効です。これはトレードオフです。早期に失効させる方法:
- ブロックリスト — 無効化したJTI(JWT ID)の値をRedisに保存し、各リクエストで確認する。
- 短い有効期限 — 5分間のトークンで被害範囲を最小化する。
- リフレッシュトークンのローテーション — ローテーション済みトークンの再利用を盗用の兆候として検出する。
JWT Decoderによるデバッグ
認証の問題をデバッグする際は、JWT Decoderを使用して以下を確認できます:
- コードを書かずにpayload全体を確認する
- 有効期限のタイムスタンプを人間が読める形式で確認する
- 使用されているアルゴリズムを確認する
- 予期しないクレームや欠落しているクレームを発見する
すべてのデコード処理はブラウザ内でローカルに行われます。トークンがあなたのマシンの外に出ることはありません。
JWTセキュリティチェックリスト
- マルチサービスアーキテクチャにはRS256またはES256を使用する
- ライブラリで許可するアルゴリズムを明示的に指定する
- すべてのリクエストで
exp、iss、audクレームを検証する - Webアプリではトークンをhttp-only cookieに保存する
- アクセストークンの有効期限を短く設定する(15分以内)
- 再利用検出機能付きのリフレッシュトークンローテーションを実装する
- payloadに機密データを入れない
- 常にHTTPSを使用する — 平文のトークンは認証がないのと同じ
JWTは強力で便利ですが、その安全性は実装次第です。構造を理解し、攻撃手法を把握することで、認証レイヤーは堅固なものになります。