2026年JWTセキュリティのベストプラクティス
最も一般的なJWT脆弱性を回避: アルゴリズム混乱、弱いシークレット、リプレイ。
JWTは数百万のシステムで認証状態を運んでおり、同じ一握りの実装ミスが完全なアカウント乗っ取り脆弱性を生み続けています。JWTのベストカレントプラクティス文書であるRFC 8725が存在するのは、まさに基本仕様(RFC 7519)が素朴に使うにはあまりに寛容だからです。実際に何が壊れるのか、各失敗をどう防ぐのかを見ていきます。
アルゴリズム混同はいまだ脆弱性ナンバーワン
トークンのヘッダーは自身のアルゴリズムを自己申告します。その申告を信頼する検証器は、攻撃者に鍵を手渡しているのと同じです。古典的な攻撃が2つ:
- alg none。 仕様は
"alg": "none"の無署名トークンを許可しています。これを受け入れるライブラリは、偽造されたペイロードを検証済みとして扱います。10年以上前の攻撃ですが、古いライブラリの新しいラッパーで再浮上し続けています。 - RS256からHS256へのダウングレード。 RS256トークンを検証するサービスは通常RSA公開鍵を持っています。攻撃者が
"alg": "HS256"のトークンを作り、その公開鍵をHMACシークレットとして署名すると、ヘッダーのalgでディスパッチするライブラリはそれを正常に検証してしまいます。公開鍵は定義上、公開されているのですから。
修正は1行で、任意ではありません。
jwt.verify(token, key, { algorithms: ['RS256'] });
サーバー側で正確なアルゴリズムリストを固定してください。トークンに選ばせてはいけません。
2026年のアルゴリズム選択
- HS256 — 対称HMAC。発行者と検証者が同一サービスなら適切。シークレットはランダムな256ビット値であること:
openssl rand -base64 32。人間が考えたパスフレーズは、トークン偽造を単一HMACへのオフライン辞書攻撃に格下げしてしまいます。 - RS256 — RSA署名。どこでもサポートされ、検証者は公開鍵だけで済みますが、署名が大きくRSA鍵生成は遅い。
- ES256 / EdDSA — 楕円曲線。トークンが小さく検証が速い。EdDSA(Ed25519)はECDSAの運用を壊してきたノンス再利用の罠も排除します。新しい分散システムにはこちらを推奨。
公開鍵はJWKSエンドポイントで配布し、新しい鍵で署名する前に先に公開するやり方でローテーションしましょう。疑うべきヘッダーフィールドが2つあります。jku と x5u は、トークンが検証鍵を任意のURLで指し示すことを可能にします。RFC 8725は明快です。厳格な許可リストにない限り無視すること。同様に kid は不透明な検索識別子として扱ってください。これをファイルパスやSQLクエリに埋め込んだ実装は、その両方の経路で悪用されてきました。
署名だけでなくすべてのクレームを検証
暗号学的に有効なトークンでも、間違ったトークンであり得ます。
const payload = jwt.verify(token, key, {
algorithms: ['ES256'],
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
clockTolerance: 5
});
- iss — 省略すると、別の(攻撃者が登録したかもしれない)テナントのトークンが通ります。
- aud — 省略すると、あるAPI向けに発行されたトークンが別のAPIに対してリプレイされます。マイクロサービス群における古典的なクロスサービス混乱した代理人バグです。
- exp / nbf — 小さなクロック許容誤差(分ではなく秒)で強制します。
- typ — RFC 9068はアクセストークンに
at+jwtを定義しています。これを確認すれば、IDトークンやリフレッシュトークンがアクセストークンの場所で受理されるのを防げます。
寿命・保存・失効は一つの設計
この3つの決定は一緒に考えて初めて意味を持ちます。
- アクセストークン: 5〜15分、メモリ内のみ。 localStorageは厳禁。あらゆるXSSが同期的に読み取り、httpOnlyフラグもそこでは無力です。
- リフレッシュトークン: httpOnly、Secure、SameSite=Strictクッキー、ローテーション付きで: 更新のたびに新トークンを発行し、旧トークンを無効化します。ローテーションで破棄済みのトークンが再提示されたら、セッション全体を失効させてください。その再利用こそが盗難の署名です。
- 失効: ステートレスなJWTは失効できません。 露出ウィンドウが許容できる程度に寿命を短く保ち、ログアウトと侵害対応のために
jtiクレームをキーとするサーバー側拒否リストを維持しましょう。プロダクトがどこでも即時かつ確実な失効を必要とするなら、それはJWTではなくセッションストア付き不透明トークンを使うべきというシグナルです。まったく立派なアーキテクチャです。
ペイロードの衛生
ペイロードはbase64urlエンコードであって暗号化ではありません。トークンを持つ誰もが、デコーダー呼び出し一回ですべてのクレームを読めます。PII、秘密にしたい権限情報、内部識別子を入れず、トークンを小さく保ちましょう。トークンはすべてのリクエストに載り、クッキー内の4KBのクレームはプロキシやCDNのヘッダー制限を超えることがあります。
デバッグチェックリスト
- デプロイ後に署名が無効: JWKSの重複期間なしに鍵ローテーションが行われた。
- ローカルでは有効、本番で拒否: クロックスキュー、またはプロキシがAuthorizationヘッダーを剥がしている。
- 毎正時に断続的な401: ドリフトするVMクロックに対してclockToleranceなしでexpを検証している。
- デコードはできるがverifyが失敗: verifyのつもりでdecodeを呼んでいます。decodeは暗号学的検査を一切行いません。
JWS、JWE、そしてJWTが実際に指すもの
厳密には、JWTはクレーム形式の名前であり、コンテナはJWS(署名付き、読み取り可能)かJWE(暗号化)のいずれかです。世の中のほぼすべての「JWT」はJWSであり、ペイロードの何ひとつ機密ではない理由がまさにここにあります。クライアントから隠すべきクレームがあるなら — 内部ユーザーフラグ、リスクスコア — トークンの背後のサーバー側に置くか、JWEを使って鍵管理の複雑さの増加を受け入れましょう。そもそも送らなければ済むデータを隠すための暗号化は、たいてい間違ったトレードです。
sdk.is/jwt-decoder のJWTデコーダーでヘッダーとクレームを安全に検査しましょう。デコードは検査であって、決して認証ではありません。