セキュリティ プログラミング ソフトウェア 公開日 2026.05.20 更新日 2026.05.20

JWT の正しい使い方と落とし穴 — 署名検証・保管・失効

JWT はステートレスで便利な反面、「alg=none」 「署名検証忘れ」 「localStorage 保管で XSS」 「失効できない」 など落とし穴も多いトークン形式です。「JWT を使うと決めた場合」 のベストプラクティス(必須クレーム検証・短命 + Rotation・HttpOnly Cookie・機密データを入れない)と、よくある事故パターンを実務目線で整理します。

先に要点

  • 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..amazonaws.com/」。

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 度使ったリフレッシュトークンが再度使われたら、「そのユーザーの全トークンを無効化」。「攻撃者がリフレッシュトークンを盗んで使った → 正規ユーザーが次のリフレッシュで再利用扱いになって検知 → 全部無効化」 のフローが成立する。

マネージド IdP に任せる

Cognito / Auth0 / Okta などの マネージド IdP は Rotation を標準で持つ。「設定で有効化」 するだけ。自前実装する場合は 「古いリフレッシュトークンの再利用検知ロジック」 を必ず実装する。

「保管場所を間違えると、いくら署名やクレームをガチガチに検証しても、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 認証は本当に必要か を読んでから判断してください。

参考リンク

あとで見返すならここで保存

読み終わったあとに残しておきたい記事は、お気に入りからまとめて辿れます。