この記事に関連するツール
ブラウザ上ですぐに試せます。記事の内容を確認しながら使うと、作業の流れをつかみやすくなります。

なぜJWTのセキュリティに注意が必要なのか
JWTは、Web開発で最も誤った実装をされやすいセキュリティ機構の一つです。ライブラリのおかげで「とりあえず動くもの」は簡単に作れます——トークンは発行でき、検証もでき、一見問題なさそうに見えます。問題が表面化するのは後になってから。セキュリティ監査のとき、インシデントのとき、あるいは攻撃者が微妙な設定ミスを突いてきたときです。
この記事では、既知の攻撃手法、JWKSによる鍵配布、本番運用の実践的なチェックリストを解説します。
おさらい:JWTとは
JWT(JSON Web Token)は、ドット(.)で区切られた3つのBase64Urlエンコード済みの部分で構成されます。
header.payload.signature
- Header:アルゴリズム(
alg)とトークン種別を指定。 - Payload:クレーム(主体に関するデータとメタデータ)を格納。
- Signature:HeaderとPayloadが改ざんされていないことの暗号学的な証明。
Payloadは エンコード されているだけで 暗号化 はされていません。JWTを傍受した者は誰でもPayloadを読めます。署名は完全性を証明するだけで、秘匿性は提供しません。
攻撃手法1:alg: none 攻撃
JWT仕様は当初、アルゴリズム値 "none"(署名なし)を許容していました。一部の実装は鍵の有無を確認せずにこれを受け入れ、攻撃者が任意のペイロードでトークンを偽造できてしまいました。
攻撃シナリオ:
- 攻撃者が正規のJWTを傍受する。
- ヘッダーとペイロードをデコードする(鍵不要のBase64Urlデコード)。
- ペイロードを改ざんする:
"role": "admin"。 "alg": "none"で再エンコードし、空の署名セグメントを送る。- 脆弱なサーバーがトークンを受け入れる。
対策:
// jose — アルゴリズムを明示的に許可リスト化
import * as jose from 'jose';
const { payload } = await jose.jwtVerify(token, secret, {
algorithms: ['HS256'], // 'none' は絶対に含めない
});
検証に使うアルゴリズムを、トークン自身が指定するアルゴリズムに依存して決めてはいけません。アルゴリズムはサーバー側で強制します。
攻撃手法2:アルゴリズム混同(RS256 → HS256)
非対称JWTは署名に秘密鍵、検証に公開鍵を使います。脆弱な実装の一部は、検証アルゴリズムをJWTヘッダーの alg フィールド——つまり 攻撃者が制御できる フィールド——から決定します。
攻撃シナリオ(公開鍵サーバー):
- サーバーは通常RS256を使う。秘密鍵で署名し、クライアントは公開鍵で検証する。
- 攻撃者はサーバーの公開鍵を知っている(公開されているのだから当然)。
- 攻撃者は
"alg": "HS256"のJWTを作る。 - 公開鍵をシークレットとしてHMAC-SHA256で署名する。
- 脆弱なサーバーはヘッダーの
alg: HS256を読んでHMAC検証に切り替え、公開鍵をHMACシークレットとして使い、偽造トークンを検証してしまう。
対策: 期待するアルゴリズムをサーバー側でハードコードし、検証方式の選択に alg クレームを信頼しないこと。
import * as jose from 'jose';
const publicKey = await jose.importSPKI(publicKeyPem, 'RS256');
const { payload } = await jose.jwtVerify(token, publicKey, {
algorithms: ['RS256'], // トークンのalgクレームを無視して固定
});
攻撃手法3:弱い/露出したシークレット
HS256(対称HMAC)では、署名と検証に同じシークレットを使います。このシークレットが、短い(32バイト未満)、推測可能(“secret” など)、公開されているソースコードにハードコードされている、といった場合、攻撃者は総当たりや推測で突破できます。一度シークレットを手に入れれば、任意のJWTを偽造できます。
対策:
- 最低256ビット(32バイト)の暗号学的に安全なランダム値を使う。
- シークレットは環境変数やシークレットマネージャー(AWS Secrets Manager、HashiCorp Vault)に保存する。
- バージョン管理にコミットしない。
# 強いシークレットを生成(Linux/Mac)
openssl rand -base64 32
攻撃手法4:クレーム検証の漏れ
署名の検証だけでは不十分です。署名が有効でも、そのトークンを このリクエストで 受け入れてよいとは限りません。
| クレーム | 検証しないと |
|---|---|
exp | 期限切れトークンが永久に受け入れられる |
nbf | 有効化前のトークンが受け入れられる |
iss | 別サービスが発行したトークンが受け入れられる |
aud | サービスA向けのトークンをサービスBが受け入れる |
const { payload } = await jose.jwtVerify(token, secret, {
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
// exp と nbf は jose が自動で検証する
});
JWKSによる鍵管理
本番システム、特に複数サービスや外部連携があるシステムでは、対称的な共有シークレットは管理しきれなくなります。トークンを検証する必要があるすべてのサービスがシークレットを知る必要があり、攻撃対象領域が広がります。
JWKS(JSON Web Key Set) は非対称暗号でこれを解決します。
- 認可サーバー が鍵ペア(秘密鍵+公開鍵)を生成する。
- 秘密鍵は認可サーバーに留まり、JWTの署名に使われる。
- 公開鍵は、標準化されたJSON形式で JWKSエンドポイント という既知のURLに公開される。
- どのサービスも、JWKSエンドポイントから公開鍵を取得してJWTを検証できる。
JWTのヘッダーには kid(Key ID)クレームが含まれ、検証側がJWKS内のどの鍵を使うかを示します。
{ "alg": "RS256", "typ": "JWT", "kid": "2026-01-key" }
JWKSの利点:
- 鍵ローテーション: 新しい鍵を追加し、古いトークンの期限切れを待ってから古い鍵を削除する——再起動やシークレット配布が不要。
- シークレット共有ゼロ: サービスは公開鍵だけを持てばよい。検証サービスが侵害されても署名能力は侵害されない。
- 複数発行者対応: 複数のJWKSエンドポイントを確認することで、複数プロバイダーのトークンを検証できる。
import * as jose from 'jose';
// joseのJWKSクライアントは鍵をキャッシュし、未知のkidで再取得する
const JWKS = jose.createRemoteJWKSet(
new URL('https://auth.example.com/.well-known/jwks.json')
);
const { payload } = await jose.jwtVerify(token, JWKS, {
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
JWKSジェネレーター を使えば、鍵ペアを作成し、どういうJSON形式で公開されるのかをブラウザ上でシミュレートできます。
トークン失効の戦略
JWTはステートレスなので、一度発行されると期限切れまで有効です。これは課題を生みます——ログアウト、パスワード変更、アカウント侵害の後など、トークンを即座に無効化したい場合はどうするのでしょうか?
戦略1:短い有効期限+リフレッシュトークン
- アクセストークンは15〜60分で期限切れ。
- リフレッシュトークン(
HttpOnlyCookie)は7〜30日。 - 失効はリフレッシュトークンのレベルだけで行えばよい。
戦略2:失効リスト(ブロックリスト)
失効した jti(JWT ID)クレームを、トークンの有効期限に合わせたTTLで高速ストア(Redis)に保存します。
// ログアウト時
await redis.set(`blocklist:${payload.jti}`, '1', { EX: remainingSeconds });
// リクエストごとに確認
const isRevoked = await redis.exists(`blocklist:${payload.jti}`);
if (isRevoked) throw new Error('トークンは失効しています');
クライアントでの安全な保存
JWTをどこに保存するかが、脆弱性の範囲を決めます。
| 保存先 | XSSリスク | CSRFリスク | 推奨 |
|---|---|---|---|
localStorage | 高 | なし | トークンには避ける |
sessionStorage | 高 | なし | トークンには避ける |
| JavaScript変数(メモリ) | 中 | なし | 短期保持ならOK |
HttpOnly Cookie | なし | あり(SameSiteで緩和) | 推奨 |
Webアプリで最も安全なのは、アクセストークンをメモリ(JavaScript変数)に、リフレッシュトークンを HttpOnly・Secure・SameSite=Strict Cookieに保存する方法です。
本番セキュリティ・チェックリスト
- 署名シークレットは最低256ビットでランダム生成されている
- シークレットはコードではなく環境変数/シークレットマネージャーに保存
-
algはサーバー側で明示指定(トークンヘッダーから読まない) -
alg: noneは決して受け入れない -
expクレームを設定(アクセストークンは15〜60分推奨) -
iss・audクレームを毎リクエスト検証 - 失効が必要なトークンには
jtiクレームを含める - トークンの発行・検証エンドポイントでHTTPSを強制
- リフレッシュトークンは
HttpOnlyCookieに保存(localStorage不可) - 複数サービスなら非対称鍵配布にJWKSを使用
- 鍵ローテーション手順を文書化・テスト済み
DevToolKitsで体感する
言葉だけでは難しい概念も、動かせば腑に落ちます。
- JWTツール:トークンがどんな「断片」でできているか、ブラウザ上で安全に覗ける。
- JWKSジェネレーター:鍵ペアを作り、公開時のJSON形式をシミュレートできる。
- ハッシュ生成ツール:SHA-256などでデータが「指紋」に変わる様子を試せる。
よくある質問
HS256とRS256のどちらを使うべき?
HS256(対称):実装が簡単で、1つのサービスが発行も検証も行う単一サービス構成に適します。鍵を使うすべての場所で秘密にする必要があります。
RS256(非対称):複数サービス構成に推奨。秘密鍵は認可サーバーだけが持ち、他のサービスはJWKS経由で公開鍵を使って検証します。設定は複雑ですが、セキュリティ面で大きく優れます。
ダウンタイムなしで鍵をローテーションするには?
新旧両方の公開鍵をJWKSエンドポイントに同時に公開します。新しいトークンは新しい鍵(新しい kid)で署名します。古い鍵で署名された古いトークンは、JWKS内の古い公開鍵で引き続き検証できます。古いトークンがすべて期限切れになったら、古い鍵をJWKSから削除します。
JWTの暗号化はある?
あります。JWE(JSON Web Encryption) はペイロードを暗号化し、復号鍵なしには中身を読めません。標準のJWT(JWS)は署名するだけで暗号化はせず、ペイロードは誰でも読めます。JWEはペイロード自体を秘匿する必要がある場合にのみ使います。
同じJWTを認証と認可の両方に使える?
両方をペイロードに含めるのは一般的です。sub クレームがユーザーを識別し(認証)、role や scope などのカスタムクレームが何をできるかを制御します(認可)。ただしトークンサイズに注意し、頻繁に変わる大量の認可データを含めるのは避けましょう。