SJ blog
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" });
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 脆弱性