DevToolBoxFREE
BlogAdvertise

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
この記事は役に立ちましたか?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Partner Picks

Sponsor this article

Place your product next to this developer topic with tracked clicks.

Ask about article sponsorship

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 でのデコード、署名アルゴリズム、セキュリティベストプラクティスを解説。

This site uses cookies for analytics and to display ads. By continuing to browse, you agree. Privacy Policy