JSON Web Token(JWT)を安全に設計・実装・運用するための指針をまとめたドキュメント。
主な根拠は RFC 7519(JWT)、RFC 7515(JWS)、RFC 7516(JWE)、RFC 8725(JSON Web Token Best Current Practices)、OWASP の各種チートシート。
目次
JWT を使うべきか判断する
署名アルゴリズム
検証処理
クレーム設計
有効期限とトークンのライフサイクル
鍵管理
クライアント側での保存と送信
暗号化(JWE)
ライブラリと実装
よくある脆弱性とアンチパターン
チェックリスト
1. JWT を使うべきか判断する
JWT は万能ではない。まず用途に合っているかを確認する。
向いている用途 向いていない用途 サービス間(マイクロサービス)の認可情報の受け渡し 単一の Web アプリのブラウザセッション管理 OAuth 2.0 / OpenID Connect のアクセストークン・ID トークン 即時失効が必須なセッション 短命なワンタイムトークン(メール確認、パスワードリセットなど) 大量のユーザー情報・権限を詰め込む用途 複数の発行者・検証者が関与する分散システム 長期間有効な認証情報
単一アプリのログインセッションであれば、サーバー側セッション+ HttpOnly Cookie の方がシンプルで失効も容易なことが多い。
「ステートレスだから」という理由だけで採用しない。失効やログアウトを実現しようとすると、結局サーバー側に状態が必要になる。
2. 署名アルゴリズム
推奨
アルゴリズム 種別 備考 EdDSA(Ed25519)非対称 高速・安全。ライブラリ対応を確認して利用 ES256(ECDSA P-256 + SHA-256)非対称 広くサポートされ、鍵・署名が小さい PS256(RSA-PSS)非対称 RSA を使うなら RS256 より推奨 RS256(RSA PKCS#1 v1.5)非対称 互換性重視の場合。鍵長は 2048bit 以上 HS256 / HS384 / HS512対称(HMAC) 発行者と検証者が同一、または完全に信頼できる場合のみ
原則
alg: none を絶対に受け入れない。
検証側で許可するアルゴリズムを明示的にホワイトリスト化する。 トークンヘッダーの alg をそのまま信用しない。
1つの鍵は1つのアルゴリズムにだけ紐づける(鍵とアルゴリズムの組を固定する)。
複数のサービスが検証する場合は非対称鍵を使う。HMAC の共有鍵を配布すると、検証者全員がトークンを偽造できてしまう。
HMAC の秘密鍵は十分なエントロピーを持つランダム値にする(HS256 なら最低 256bit)。パスワードや推測可能な文字列を使わない。
3. 検証処理
トークンを受け取ったら、以下をすべて 検証する。どれか1つでも省略すると脆弱性になる。
形式 :3つ(JWS)または5つ(JWE)の Base64URL セグメントで構成されているか
アルゴリズム :alg が許可リストに含まれているか
署名 :正しい鍵で署名が検証できるか
exp(有効期限) :期限切れでないか
nbf(有効開始時刻) :まだ有効になっていないトークンでないか
iat(発行時刻) :未来の時刻や極端に古い値でないか(必要に応じて)
iss(発行者) :期待する発行者と完全一致するか
aud(対象者) :自サービスが含まれているか
typ / トークン種別 :期待する種類のトークンか(ID トークンをアクセストークンとして受け入れない等)
アプリ固有のクレーム :スコープ、テナント ID、権限など
補足
時刻検証には小さなクロックスキュー許容(数十秒〜数分程度)を設ける。大きくしすぎない。
デコードと検証を混同しない。 jwt.decode() のように署名を検証しない関数の結果を信用してはいけない。
検証に失敗したら、理由をクライアントに詳しく返さない(ログには残す)。
kid ヘッダーで鍵を選ぶ場合、kid の値で任意のファイルパスや SQL を参照しないこと(インジェクション対策)。既知の鍵セットから引くだけにする。
jku / x5u / jwk ヘッダーで指定された鍵を無条件に取得・信用しない。使うなら URL を許可リストで固定する。
4. クレーム設計
登録済みクレーム
クレーム 用途 推奨 iss発行者 必須。URL 形式で一意に sub主体(ユーザー ID 等) 必須。変化しない内部 ID を使い、メールアドレスは避ける aud対象者 必須。検証側で必ずチェック exp有効期限 必須 iat発行時刻 推奨 nbf有効開始時刻 必要に応じて jtiトークン ID 失効管理・リプレイ対策をするなら必須
原則
JWS のペイロードは暗号化されていない。 Base64URL でエンコードされているだけで誰でも読める。パスワード、個人情報、機密データを入れない。
ペイロードは最小限にする。トークンサイズが大きいとヘッダー長制限や通信量の問題が起きる。
頻繁に変わる情報(権限の詳細、プロフィールなど)は入れず、必要ならサーバー側で引く。
用途ごとにトークンを区別する。typ ヘッダー(例: at+jwt、RFC 9068)や専用クレームで種別を明示し、別用途のトークンが流用されないようにする。
同じ鍵で複数種類のトークンを発行する場合は、aud や typ で確実に区別できるようにする。
5. 有効期限とトークンのライフサイクル
有効期限の目安
トークン 目安 アクセストークン 5〜15分 ID トークン 数分〜1時間(ログイン直後の検証用) リフレッシュトークン 数時間〜数週間(用途とリスクに応じて) ワンタイムトークン(メール確認等) 数分〜24時間
アクセストークン+リフレッシュトークン
アクセストークンは短命にし、失効の遅延を最小化する。
リフレッシュトークンはサーバー側で状態管理 し、失効できるようにする。
リフレッシュトークンローテーション :使用のたびに新しいリフレッシュトークンを発行し、古いものを無効化する。
再利用検知 :無効化済みのリフレッシュトークンが使われたら、そのトークンファミリー全体を失効させる(盗難の兆候)。
リフレッシュトークンは可能なら不透明な(opaque)ランダム文字列にし、JWT にする必要はない。
失効(Revocation)
JWT は本来、期限まで有効であり続ける。即時失効が必要な場合は以下を組み合わせる。
jti によるブロックリスト(期限切れまで保持すればよい)
ユーザー単位の「トークン世代番号」や「最終ログアウト時刻」と iat の比較
パスワード変更・権限変更・アカウント停止時の強制失効
高リスク操作では、トークンだけでなく再認証やサーバー側の状態確認を併用する
6. 鍵管理
秘密鍵は KMS / HSM / シークレットマネージャーで管理し、ソースコードやリポジトリに含めない。
鍵ローテーション を前提に設計する。
kid ヘッダーで鍵を識別する。
公開鍵は JWKS エンドポイント(/.well-known/jwks.json 等)で公開する。
新しい鍵を先に JWKS に追加 → 新しい鍵で署名開始 → 旧鍵で署名されたトークンの期限切れを待って旧鍵を削除、の順で切り替える。
検証側は JWKS をキャッシュしつつ、未知の kid が来たら再取得する(ただし再取得の頻度は制限する)。
鍵漏洩時の手順(即時ローテーション、全トークン失効)をあらかじめ決めておく。
環境(本番・ステージング・開発)ごとに鍵を分ける。
7. クライアント側での保存と送信
ブラウザでの保存場所
保存場所 XSS CSRF 備考 HttpOnly + Secure + SameSite CookieJS から読めない SameSite と CSRF 対策で軽減推奨 メモリ(JS 変数) 盗まれうるが永続しない 影響なし SPA のアクセストークン向け。リロードで消える localStorage / sessionStorageXSS で容易に盗まれる 影響なし 非推奨
Cookie を使う場合は HttpOnly、Secure、SameSite=Lax または Strict、適切な Path / Domain を設定する。
SPA では BFF(Backend for Frontend)パターン を検討する。トークンはサーバー側に保持し、ブラウザとは Cookie セッションでやり取りする。
送信
常に HTTPS(TLS)で送信する。
API では Authorization: Bearer <token> ヘッダーを使う。
URL のクエリパラメータにトークンを含めない。 アクセスログ、ブラウザ履歴、Referer ヘッダーから漏洩する。
トークンをログに出力しない(出す場合はマスクする)。
トークン窃取への追加対策
送信者制約付きトークン(Sender-Constrained Token)を検討する。
DPoP (RFC 9449):クライアントの鍵ペアにトークンを紐づける
mTLS (RFC 8705):クライアント証明書にトークンを紐づける
8. 暗号化(JWE)
ペイロードの内容を第三者(クライアント自身を含む)に見せたくない場合は JWE を使う。
署名と暗号化の両方が必要な場合は Sign-then-Encrypt (JWS を JWE で包む、ネストした JWT)にする。
推奨アルゴリズムの例:
鍵管理: ECDH-ES+A256KW、RSA-OAEP-256
コンテンツ暗号化: A256GCM
RSA1_5 は既知の攻撃があるため使わない。
多くのケースでは、「機密情報をトークンに入れない」方が JWE を導入するより簡単で安全。
9. ライブラリと実装
JWT を自前実装しない。 実績のあるメンテナンスされたライブラリを使う。
例: jose(Node.js)、PyJWT / joserfc(Python)、golang-jwt/jwt / go-jose(Go)、jjwt / Nimbus JOSE+JWT(Java)
ライブラリのデフォルト設定を過信しない。許可アルゴリズム、iss、aud は明示的に指定する。
ライブラリを常に最新に保ち、脆弱性情報(CVE)を追う。
検証処理を共通のミドルウェア/モジュールに集約し、エンドポイントごとにばらばらに実装しない。
検証ロジックに対して、改ざん・期限切れ・alg: none・誤った aud などの異常系テストを書く。
10. よくある脆弱性とアンチパターン
問題 内容 対策 alg: none 攻撃署名なしトークンを受け入れてしまう 許可アルゴリズムを明示 アルゴリズム混同攻撃 RS256 の公開鍵を HS256 の秘密鍵として使わせ、偽造される 鍵とアルゴリズムを固定し、alg ヘッダーを信用しない 弱い HMAC 鍵 短い・推測可能な秘密鍵がオフラインで総当たりされる 256bit 以上のランダム鍵 署名未検証 デコードだけして中身を信用する 必ず検証関数を使う aud / iss 未検証別サービス向けのトークンが通る 必ず検証 kid インジェクションkid 経由でパストラバーサルや SQL インジェクション既知の鍵セットからのみ参照 jku / x5u の悪用攻撃者の用意した鍵で検証させられる URL の許可リスト化、または不使用 機密情報の格納 ペイロードが誰でも読める 機密情報を入れない/JWE 長すぎる有効期限 漏洩時の被害が長期化 短命アクセストークン+リフレッシュ localStorage 保存XSS で盗まれる HttpOnly Cookie / BFFURL にトークン ログや Referer で漏洩 ヘッダーで送信 トークン種別の混同 ID トークンをアクセストークンとして受け入れる等 typ・aud で区別
11. チェックリスト
発行側
検証側
運用・クライアント
参考資料