backend
S
信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
JWTの落とし穴:セキュアな認証設計のために
JWT(JSON Web Token)の仕組みと、よくある実装ミスを解説。アルゴリズム混乱攻撃・トークン失効問題・シークレット管理など、セキュアに使うための実践的なガイドです。
一言結論
JWTはアルゴリズムを明示指定し、有効期限を短く設定してRefresh Tokenで失効管理する設計にしないと、アルゴリズム混乱攻撃やログアウトが効かない問題に直結する。
JWT とは
JWT(JSON Web Token)は ヘッダー.ペイロード.署名 の3パートで構成される、自己完結型のトークンです。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWxpY2UiLCJpYXQiOjE2MjM5MDY4MDB9
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
サーバーはシークレットで署名し、改ざんを検知できます。データベースに保存不要で、ステートレスな認証を実現します。
落とし穴1: アルゴリズム混乱攻撃
alg: none や RS256 のトークンを HS256 として検証させる攻撃です。
// ❌ alg を信頼してしまう実装
const decoded = jwt.verify(token, secret); // alg はトークン内のものを使う
// ✅ アルゴリズムを明示的に指定する
const decoded = jwt.verify(token, secret, { algorithms: ["HS256"] });
alg: none のトークンを受け付けないライブラリを使うか、必ずアルゴリズムを明示してください。
落とし穴2: トークンの失効ができない
JWT は自己完結型のため、発行済みトークンをサーバー側から無効化できません。
シナリオ: ユーザーがログアウト or パスワード変更
→ まだ有効な JWT を持つ攻撃者は引き続き API を使える
対策1: 有効期限を短くする(推奨)
// Access Token: 15分
const accessToken = jwt.sign(payload, secret, { expiresIn: "15m" });
// Refresh Token: 7日(データベースに保存して失効管理)
const refreshToken = jwt.sign({ sub: userId }, refreshSecret, { expiresIn: "7d" });
await db.refreshTokens.create({ token: refreshToken, userId });
対策2: ブロックリスト(Redis)
// ログアウト時
await redis.setex(`blocklist:${jti}`, tokenTTL, "1");
// 検証時
const isBlocked = await redis.get(`blocklist:${jti}`);
if (isBlocked) throw new Error("Token revoked");
落とし穴3: シークレットが弱い・露出している
// ❌ 弱いシークレット
const secret = "password";
const secret = "mysecret";
// ✅ ランダムな256bit以上のシークレット
const secret = process.env.JWT_SECRET; // 環境変数から取得
// openssl rand -base64 32 で生成
.env ファイルの管理:
- .gitignore に必ず追加
- 本番環境ではシークレットマネージャーを使用
- 定期的にローテーション
落とし穴4: ペイロードに機密情報を入れる
JWT のペイロードはBase64 エンコードされているだけで、暗号化されていません。
// ❌ 機密情報をペイロードに含める
const token = jwt.sign({
userId: "123",
password: "secret", // 絶対NG
creditCard: "1234...", // 絶対NG
}, secret);
// ✅ 最小限の情報のみ
const token = jwt.sign({
sub: "123", // ユーザーID(標準クレーム)
role: "admin", // 必要な権限情報のみ
iat: Date.now(), // 発行時刻(自動付与)
}, secret, { expiresIn: "15m" });
落とし穴5: Cookie と localStorage の誤った選択
localStorage:
✅ JavaScript からアクセスしやすい
❌ XSS でトークンを盗まれるリスク
httpOnly Cookie:
✅ JavaScript からアクセス不可(XSS に強い)
✅ Secure + SameSite=Strict で CSRF も防御
❌ モバイルアプリから扱いにくい(別途対策必要)
// ✅ httpOnly Cookie に保存する(Webアプリ推奨)
res.cookie("accessToken", token, {
httpOnly: true,
secure: true, // HTTPS 必須
sameSite: "strict", // CSRF 防止
maxAge: 15 * 60 * 1000, // 15分
});
まとめ
| 問題 | 対策 |
|---|---|
| アルゴリズム混乱 | algorithms を明示指定 |
| トークン失効 | 短い有効期限 + Refresh Token |
| 弱いシークレット | openssl rand -base64 32 で生成 |
| 機密情報の漏洩 | ペイロードは最小限 |
| XSS による窃取 | httpOnly Cookie に保存 |
参考: JWT 公式 / OWASP - JWT 脆弱性