先に要点
- JWT(JSON Web Token) は 「署名付きの JSON 文字列」。ステートレスで便利だが、仕様の誤解と保管方法のミスで事故りやすい トークン形式。「JWT を使うと決めた」 時に最低限守るべきベストプラクティスを意識して実装する。
- 必ず守る 5 項目: ① 署名検証(「alg=none」 を弾く・期待アルゴリズムを固定)、② iss / aud / exp / nbf の検証、③ 短命アクセストークン + リフレッシュトークン Rotation、④ HttpOnly Cookie or 安全な保管、⑤ 機密データを入れない(JWT は base64 で誰でも中身が読める)。
- 典型事故: alg=none で偽造」 「kid パラメータ操作で署名検証バイパス」 「公開鍵を秘密鍵として受け取る攻撃」 「localStorage 保管で XSS から全部流出」 `失効できないので退会後もトークン有効。多くは 仕様の誤解 と ライブラリの設定不足 が原因。
- 「 JWT は失効できない」 という制約を理解する。「サーバ側の失効リスト(ブラックリスト)」 を持つか、「アクセストークンを短命に + リフレッシュ時に確認」 で実質的な失効を実現する。「ログアウト = リフレッシュトークン削除 + アクセストークン期限切れ待ち」 の設計が現実的。
- JWT を自分で実装しない が現代の正解。「jsonwebtoken (Node)」 「firebase/php-jwt」 「PyJWT」 のような メンテされているライブラリ を使い、`設定で 「期待アルゴリズム」 「期待 iss」 「期待 aud」 を必ず指定する。「手書きの署名検証」 はほぼ事故る。
「JWT で認証実装しよう」 と書き始めた瞬間に、仕様の落とし穴に踏み込む可能性が高い のが現実です。「alg=none」 「localStorage 保管で全流出」 「失効できない」 など、JWT 由来の事故事例は世界中で繰り返されています。
ざっくり言うと、JWT は 署名付き JSON 文字列」 で、ステートレスにユーザー情報を運べる便利さがある一方、`正しい使い方を理解しないと脆弱性を生む 仕様です。「JWT が必要かそもそも判断」 については JWT 認証は本当に必要か に整理してあるので、この記事は 使うと決めた場合の正しい使い方 を整理します。
「OAuth 2.0 / OIDC で JWT を扱う」 「AWS Cognito から発行された JWT を検証する」 のような実装シーンで、「これは必ず守る」 と言えるベストプラクティスを 5 項目に整理し、よくある事故パターンと対策を解説します。
まず JWT の構造を一言で
JWT は 3 つの部分が 「.」 で繋がれた base64 文字列 です。
Header(ヘッダ)
「 alg(アルゴリズム)」 「typ(型)」 「kid(鍵 ID)」 などのメタ情報。「どんな署名アルゴリズムで署名されたか」 を示す。ここを信用するのが事故の元。
Payload(ペイロード)
「 sub」 「iss」 「aud」 「exp」 「nbf」 「iat」 のような標準クレーム + カスタムクレーム。base64 で誰でも読める(暗号化ではない)。「機密情報を入れない」 が鉄則。
Signature(署名)
Header + Payload に対するデジタル署名。改ざんを検知するため」 にあり、`内容を秘匿するためではない。HS256(共有鍵)/ RS256(公開鍵 + 秘密鍵)/ ES256 などの方式。
特性
「 自己完結(中身を見ればユーザー情報がわかる)」 「ステートレス(サーバが状態を持たない)」 「期限を含む」 が特徴。一方、失効できない」 `中身は見えるので機密情報には不向き の制約も持つ。
「暗号化されている」 という誤解が事故の出発点になりやすいので、最初に 「署名 = 改ざん検知、Payload は誰でも読める」 を頭に入れます。
必ず守る 5 項目
JWT を使うときに 絶対に外せない プラクティスを 5 つに整理します。
① 署名検証 — 「alg=none」 を絶対に通さない
JWT 史上最も有名な脆弱性パターン。
事故パターン
` 攻撃者が JWT の Header を 「alg: none」 に書き換え、Signature を空にして送る」。古い / 設定不足のライブラリは `none アルゴリズムで 「署名検証なし」 として通してしまう」。誰でも任意のユーザーになりすませる致命的脆弱性。
対策
ライブラリで 期待するアルゴリズムを明示的に指定(「RS256 のみ」 「HS256 のみ」 のように)。「alg=none」 は常に拒否。トークンの Header の alg をそのまま信じない が原則。
② iss / aud / exp / nbf の検証
「署名が通れば OK」 では不十分。クレームの検証も必須。
iss(Issuer / 発行者)
「 この JWT は本当に自分が信頼する IdP が発行したか」 を確認。期待する iss と一致しないトークンは拒否。例: Cognito なら 「https://cognito-idp.
aud(Audience / 発行先)
「 この JWT は自分のアプリ宛か」 を確認。他のアプリ宛のトークンを誤って受け入れる(OAuth Confused Deputy 攻撃)を防ぐ。OIDC の ID トークン検証では必須。
exp(Expiration / 有効期限)
「 期限切れトークンを拒否」。クロックスキュー(時計のずれ)」 を考慮して数十秒の許容範囲を入れるのが実用的。
nbf(Not Before)
「 有効になる開始時刻」。「未来の時刻に有効化されるトークン」 を期限前に使えないようにする。「遅延発効が必要な特殊用途」 で使う。「一般的なログイントークン」 では設定しないか即時。
ライブラリで 「 issuer / audience / clockSkew」 をオプションで渡せば自動で検証してくれます。
③ 短命アクセストークン + リフレッシュトークン Rotation
「一度発行したトークンを長く有効にしない」 が漏洩時の被害縮小の鍵。
アクセストークンは短命に
「 15 分〜1 時間」 が一般的。「漏洩しても短時間で無効化」 され、攻撃可能時間が限定される。「UX を犠牲にしないため」 にリフレッシュトークンで自動再発行する設計。
リフレッシュトークンは長期 + Rotation
「 日 〜 週 単位」 の長期有効。使うたびに新しいリフレッシュトークンを発行し、古いものを無効化(Token Rotation) を有効化する。「漏洩したリフレッシュトークンが再利用されたら検知できる」 仕組みを兼ねる。
Rotation の再利用検知
` 1 度使ったリフレッシュトークンが再度使われたら、「そのユーザーの全トークンを無効化」。「攻撃者がリフレッシュトークンを盗んで使った → 正規ユーザーが次のリフレッシュで再利用扱いになって検知 → 全部無効化」 のフローが成立する。
④ HttpOnly Cookie か安全な保管
「保管場所を間違えると、いくら署名やクレームをガチガチに検証しても、JWT 自体が漏洩する」。
localStorage は NG
「 localStorage に JWT を保管すると、XSS 一発で全部漏れる」。JavaScript からアクセス可能なので、「攻撃者が任意の JS を注入できた瞬間にトークン取得 → サーバへ送信」 が成立。
HttpOnly + Secure + SameSite Cookie
「 HttpOnly Cookie」 は JavaScript からアクセス不可。「Secure」 で HTTPS 限定、「SameSite=Lax / Strict」 で CSRF 対策。「バックエンドが自動で Cookie を読んで認証」 する設計が安全。
BFF パターン
「 フロント(SPA) → 自社バックエンド(BFF) → 認可サーバ」 の構成にし、フロントには Cookie だけ、トークンはバックエンドが保管 する。「SPA で JWT を扱う現代的なベストプラクティス」。
モバイルアプリ
「 iOS Keychain / Android Keystore」 のような OS のセキュアストレージに保管。「平文の SharedPreferences / NSUserDefaults」 は NG。プラットフォームの推奨 API を使う。
⑤ 機密データを入れない
JWT の Payload は 暗号化されておらず base64 で誰でも読める。
入れて OK なもの
「 ユーザー識別子(sub)」 「ロール / 権限(roles)」 「テナント ID」 「表示名 / メール(認証用途で必要なら)」 程度。漏洩しても致命的でない情報 に限る。
入れてはダメなもの
「 パスワード」 「クレジットカード番号」 「マイナンバー」 「健康情報」 「秘密の質問の答え」 のような 機密情報。「JWT は暗号化されていない」 ことを忘れがちなので明示的に意識する。
Payload は最小に
「 必要最小限のクレームに絞る」 のが原則。「大きなクレームを大量に詰めると、リクエストごとに送信するヘッダサイズが膨張」 し、「HTTP の限界超えやパフォーマンス劣化」 を招く。「詳細情報は API 側で別途取得」 する設計。
本当に必要なら JWE
「 機密情報を JWT に入れたい」 場合は JWE(JSON Web Encryption) を使う。JWT(署名のみ) と JWE(暗号化) は別仕様。「JWT を JWE で包む」 と署名 + 暗号化が両立する。実装複雑度が上がるので 「本当に必要か」 を考えてから。
よくある事故パターン
ベストプラクティスを守らないと起きる、典型的な事故を整理します。
| 事故パターン | 原因 | 対策 |
|---|---|---|
| alg=none で誰でもなりすまし | ライブラリで期待アルゴリズム未指定 | 期待 alg を明示し、none を拒否 |
| HMAC キーで RSA 公開鍵を渡す攻撃 | RS256 を期待したのに HS256 のトークンを受け入れ、「公開鍵を秘密鍵として検証」 | 期待 alg を 1 種類に固定。アルゴリズムを Header から取らない |
| kid 操作で別バケット読み取り | 「 kid」 をそのままファイルパスに使い、「../../secret」 のようなパスインジェクション | 「kid」 はホワイトリスト検証 or JWKS の URL を固定 |
| localStorage の JWT が XSS で流出 | XSS 経由で localStorage から全トークン取得 | HttpOnly Cookie + BFF パターンへ移行 |
| 退会したユーザーのトークンが残り続ける | JWT に失効機構がなく、「期限切れまで有効」 状態 | 短命 + リフレッシュ無効化、または DB 側のセッション ID 管理 |
| クロックスキューで認証失敗が頻発 | サーバ時刻が数秒ずれていて期限境界で失敗 | クロックスキュー許容(数十秒)を設定。NTP で時刻同期 |
| JWT のサイズ膨張で 431 エラー | Payload に大量のクレームを詰めて HTTP ヘッダが上限超過 | 必要最小限のクレームに絞る |
| 機密情報を Payload に入れて漏洩 | JWT が 「暗号化」 と誤解してパスワードや個人情報を入れた | JWT は署名のみと理解。必要なら JWE で包む |
JWT の失効問題と対処
「JWT は失効できない」 という制約は、設計初期に必ず考慮します。
「一般的な Web/API なら短命 + Rotation」 で十分、「即時失効が必要な要件(金融、健康データ)なら DB 併用」 のような使い分けが現実的です。
JWT を扱う時の最終チェックリスト
実装時に必ず通すチェックリストを整理します。
8 項目すべてを満たして初めて 「JWT を安全に使えている」 と言えます。
JWT に関するよくある質問
Q. JWT は暗号化されていますか?
A. 暗号化されていません。JWT(JWS)は 署名付きの base64 文字列で、「誰でも中身を読める」。「暗号化したい」 なら JWE(JSON Web Encryption)を使うか、「JWT を JWE で包む」 構成にします。「Payload に機密情報を入れないのが鉄則」 です。
Q. HS256 と RS256 はどっちを使うべき?
A. IdP がトークンを発行し、複数のサービスが検証する」 なら RS256(非対称)、「単一サービス内で完結」 なら HS256(共有鍵) が基本。OIDC / マネージド IdP は ` RS256 標準で、公開鍵は JWKS で配布されます。「HS256 を複数サービスで共有」 は鍵管理の難しさで事故りやすい。
Q. JWT を localStorage に置いてはいけない?
A. 原則 NGです。XSS 一発で全トークンが流出するリスクがあるため、HttpOnly Cookie + BFF パターンが現代の推奨。「どうしても SPA で JWT を扱いたい」 場合は、「In-Memory(リロードで消える)+ HttpOnly Cookie のリフレッシュトークン」 のような ハイブリッド構成もあります。
Q. JWT の有効期限はどれくらいが適切?
A. アクセストークン 15〜60 分、リフレッシュトークン 数日〜数週間 が一般的。「機密度が高い操作(送金など)」 は 数分の超短命トークン + 再認証要求 のような追加対策。「UX を犠牲にしすぎない」 のが現実的な落としどころ。
Q. JWT のセッション失効はどう実装すべき?
A. 一般的な Web では 「短命 + リフレッシュ無効化」 で十分。「即時失効必須」 なら ブラックリスト方式(jti を Redis 等で管理) か DB セッション併用(JWT の sub から DB を引く)。要件次第で使い分け。
Q. クロックスキューって何ですか?
A. サーバ間の時計のずれ です。「発行サーバと検証サーバの時計が数秒ずれている」 と、「まだ有効なはずのトークンが期限切れ扱い」 になります。ライブラリの 「clockTolerance(数十秒)」 オプションで許容する、「NTP で時刻同期する」 の両方で対応します。
Q. JWT のサイズが大きくなりすぎたらどうしますか?
A. Payload を最小限に絞るのが第一。「大量のロール / 詳細プロフィール / 権限リスト」 を全部詰めると、HTTP ヘッダの上限(8〜16KB)を超える 「431 Request Header Fields Too Large」 エラーになります。認証用の最小情報だけ JWT、詳細は API で別途取得 が定石。「セッション ID を JWT に入れて、サーバで詳細を引く」 ハイブリッドも有効。
まとめ
JWT は 便利だが仕様の誤解と保管方法のミスで事故りやすい トークン形式です。「5 つのベストプラクティス」(署名検証・クレーム検証・短命 + Rotation・HttpOnly Cookie・機密データを入れない)を守るだけで、大半の典型事故を構造的に避けられます。
「JWT を自前実装しない」 「メンテされたライブラリを設定で正しく使う」 「マネージド IdP に任せる」 の 3 原則で、認証実装の品質と運用負荷の両方を改善できます。「JWT を使うかどうか」 から迷っているなら、JWT 認証は本当に必要か を読んでから判断してください。
参考リンク
- IETF: RFC 7519 - JSON Web Token (JWT)
- IETF: RFC 8725 - JWT Best Current Practices
- OWASP: JSON Web Token Cheat Sheet
- Auth0: JWT.io
- IETF: RFC 7515 - JSON Web Signature (JWS)