DevToolBox無料
ブログ

JWTセキュリティベストプラクティス: アルゴリズム選択、有効期限、ローテーション

14分by DevToolBox

JWTセキュリティのベストプラクティス:アルゴリズム選択、有効期限、ローテーション、攻撃防御

JSON Web Tokenはいたるところで使われています — しかし、ほとんどの実装には重大なセキュリティの欠陥があります。このガイドではアルゴリズム選択、トークンの有効期限とローテーション、リフレッシュトークンパターン、主要なJWT攻撃への防御方法を解説します。

JWTとは何か、どのように機能するか?

JWTはBase64URLエンコードされた文字列で、ドットで区切られた3つの部分で構成されています:ヘッダー(アルゴリズム+タイプ)、ペイロード(クレーム)、署名。

アルゴリズム選択:HS256 vs RS256 vs ES256

最も一般的なJWTセキュリティのミスは、分散システムでHS256を使用することです。RS256とES256は非対称鍵を使用します。

// Anatomy of a JWT — Header.Payload.Signature

// 1. Header (Base64URL decoded)
const header = {
  alg: "RS256",   // Algorithm: RS256 (asymmetric) preferred over HS256 (symmetric)
  typ: "JWT"
};

// 2. Payload (Base64URL decoded) — the claims
const payload = {
  iss: "https://auth.example.com",         // Issuer
  sub: "user_01HXYZ123",                  // Subject (user ID)
  aud: "https://api.example.com",          // Audience (target service)
  exp: 1735689600,                         // Expiry: Unix timestamp (15 min from now)
  iat: 1735688700,                         // Issued at
  jti: "8f14e0f9-21af-4c1e-8f26-3b4a5c6d7e8f", // Unique token ID (for revocation)
  roles: ["user"],                         // Custom claims
  email: "alice@example.com"
};

// 3. Signature (server-side only)
// RS256: sign(base64url(header) + '.' + base64url(payload), PRIVATE_KEY)
// Verification: verify(token, PUBLIC_KEY) — other services only need the public key

// --- Generating a JWT with jsonwebtoken (Node.js) ---
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
import { randomUUID } from 'crypto';

const privateKey = readFileSync('./private.pem');   // RSA private key
const publicKey  = readFileSync('./public.pem');    // RSA public key (shared with all services)

function issueAccessToken(userId: string, roles: string[]): string {
  return jwt.sign(
    {
      sub: userId,
      aud: 'https://api.example.com',
      roles,
      jti: randomUUID(),
    },
    privateKey,
    {
      algorithm: 'RS256',
      issuer: 'https://auth.example.com',
      expiresIn: '15m',  // Short-lived access token
    }
  );
}

function verifyAccessToken(token: string) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],           // CRITICAL: whitelist algorithms — never allow 'none'
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
  });
}

トークンの有効期限とローテーション戦略

短命のアクセストークン(15分)と長命のリフレッシュトークン(7〜30日)のペアが業界標準です。

// Refresh Token Pattern — Stateless Access + Stateful Refresh

import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';

// ---- Token issuance ----
async function issueTokenPair(userId: string) {
  const accessToken = jwt.sign(
    { sub: userId, jti: randomUUID() },
    process.env.JWT_PRIVATE_KEY!,
    { algorithm: 'RS256', expiresIn: '15m' }
  );

  // Refresh token: random bytes stored in DB — NOT a JWT
  const refreshToken = randomBytes(64).toString('hex');
  const refreshExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days

  // Store hashed refresh token in DB (never store plaintext)
  await db.refreshTokens.create({
    tokenHash: sha256(refreshToken),
    userId,
    expiresAt: refreshExpiry,
    createdAt: new Date(),
  });

  return { accessToken, refreshToken, refreshExpiry };
}

// ---- Token refresh endpoint ----
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies; // httpOnly cookie
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  const stored = await db.refreshTokens.findOne({
    tokenHash: sha256(refreshToken),
    expiresAt: { >: new Date() }, // Not expired
    revokedAt: null,                  // Not revoked
  });

  if (!stored) return res.status(401).json({ error: 'Invalid or expired refresh token' });

  // Rotate: invalidate old, issue new pair (refresh token rotation)
  await db.refreshTokens.update(
    { id: stored.id },
    { revokedAt: new Date() }         // Revoke old refresh token
  );

  const newPair = await issueTokenPair(stored.userId);

  // Set new refresh token as httpOnly cookie
  res.cookie('refreshToken', newPair.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    expires: newPair.refreshExpiry,
    path: '/auth/refresh',            // Scope cookie to refresh endpoint only
  });

  return res.json({ accessToken: newPair.accessToken });
});

// ---- Logout: revoke refresh token ----
app.post('/auth/logout', async (req, res) => {
  const { refreshToken } = req.cookies;
  if (refreshToken) {
    await db.refreshTokens.update(
      { tokenHash: sha256(refreshToken) },
      { revokedAt: new Date() }
    );
  }
  res.clearCookie('refreshToken', { path: '/auth/refresh' });
  return res.json({ success: true });
});

一般的なJWT攻撃と防御

最も危険なJWT脆弱性:アルゴリズム混乱、RS256→HS256混乱、弱い秘密鍵、exp検証の欠如。

// JWT Attack Defenses — Code Patterns

// ---- ATTACK 1: Algorithm confusion ("none" bypass) ----
// VULNERABLE: never use verify() without specifying allowed algorithms
const VULNERABLE = jwt.verify(token, secret); // attacker can set alg: "none"

// SAFE: always whitelist algorithms
const SAFE = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Never include 'none', 'HS256' if you're using RS256
});

// ---- ATTACK 2: RS256 > HS256 confusion ----
// Attacker changes alg from RS256 to HS256 and signs with the PUBLIC key as the secret
// Defense: always specify algorithm on verify — never infer from the token header
// VULNERABLE:
const header = JSON.parse(atob(token.split('.')[0]));
jwt.verify(token, publicKey, { algorithms: [header.alg] }); // attacker controls this!

// SAFE:
jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // hardcoded

// ---- ATTACK 3: Weak HS256 secrets (brute-force) ----
// VULNERABLE: short or predictable secrets
const BAD_SECRET = 'secret';
const BAD_SECRET2 = 'mysupersecretkey';

// SAFE: 256-bit cryptographically random secret
import { randomBytes } from 'crypto';
const GOOD_SECRET = randomBytes(32).toString('hex'); // 256-bit
// Store in environment variable, rotate annually

// ---- ATTACK 4: Missing expiry validation ----
// Some older libraries skip exp check by default
// SAFE: always validate exp (jsonwebtoken does this by default)
try {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    clockTolerance: 30, // Allow 30 seconds clock skew max
    ignoreExpiration: false, // NEVER set this to true in production
  });
} catch (err) {
  if (err instanceof jwt.TokenExpiredError) {
    return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
  }
  return res.status(401).json({ error: 'Invalid token' });
}

// ---- Token blacklist (for early revocation via jti) ----
import { createClient } from 'redis';
const redis = createClient();

async function revokeToken(jti: string, exp: number) {
  const ttl = exp - Math.floor(Date.now() / 1000); // seconds until natural expiry
  if (ttl > 0) {
    await redis.set(`jwt:revoked:${jti}`, '1', { EX: ttl });
  }
}

async function isTokenRevoked(jti: string): Promise<boolean> {
  return (await redis.get(`jwt:revoked:${jti}`)) !== null;
}

// Middleware: check blacklist
app.use(async (req, res, next) => {
  const decoded = jwt.verify(req.headers.authorization?.split(' ')[1] ?? '', publicKey, {
    algorithms: ['RS256'],
  });
  if (await isTokenRevoked((decoded as jwt.JwtPayload).jti ?? '')) {
    return res.status(401).json({ error: 'Token revoked' });
  }
  next();
});

セキュアなトークン保存

アクセストークン:メモリに保存。リフレッシュトークン:httpOnly、Secure、SameSite=StrictのCookieに保存。

よくある質問

JWTとopaque セッショントークンのどちらを使うべきか?

サーバーサイドレンダリングアプリにはopaqueセッショントークンを使用。マイクロサービスやAPIにはJWTを使用。

有効期限前にJWTを失効させるには?

解決策:短い有効期限(15分)、Redisでのトークンブラックリスト、ユーザーレコードのトークンバージョン。

すべてのJWTに含まれるべきクレームは?

必須:iss、sub、aud、exp、iat、jti。受信サービスでissとaudを必ず検証してください。

JWTの秘密鍵と秘密鍵の長さは?

HS256の秘密鍵:最低256ビット。RS256:最低2048ビットのRSA鍵。ES256:256ビットの楕円曲線鍵。

関連ツール

𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

最新情報を受け取る

毎週の開発ヒントと新ツール情報。

スパムなし。いつでも解除可能。

Try These Related Tools

JWTJWT Decoder→BBase64 Decoder{ }JSON Formatter

Related Articles

JWT認証:完全実装ガイド

JWTをゼロから実装。トークン構造、アクセストークンとリフレッシュトークン、Node.js実装、クライアント側管理、セキュリティベストプラクティス、Next.jsミドルウェア。

HTTPヘッダー完全ガイド:リクエスト、レスポンス、セキュリティヘッダー

HTTPヘッダーの完全ガイド:リクエスト、レスポンス、セキュリティヘッダーを解説。

REST API設計ガイド:2026年のベストプラクティス

堅牢なREST API設計の包括的ガイド:リソース命名、HTTPメソッド、ページネーション、エラー処理、認証、キャッシュ。

オンライン JWT デコーダー:JSON Web Token のデコード、検査、デバッグ(2026年版ガイド)

無料のオンライン JWT デコーダーで JWT ヘッダー、ペイロード、クレームを即座に検査。JWT 構造、標準クレーム、JavaScript/Python/Go/Java でのデコード、署名アルゴリズム、セキュリティベストプラクティスを解説。