mcpField
J
jwt-security

jwt-security

github.com/mindrallyセキュリティ2871.6k 回インストール3 週間前に更新
リポジトリ

jwt-security について

JWT による認証と認可を実装する開発者向けのセキュリティガイドです。トークンの作成・検証、鍵管理、ライフサイクル制御、攻撃への対策を扱います。

主な機能

  1. 01署名アルゴリズムと鍵の選択
  2. 02クレーム確認とトークン検証
  3. 03更新・失効・鍵ローテーション
  4. 04安全な保存と送信の指針
  5. 05脆弱性とアンチパターンへの対策

活用シーン

  1. 01JWT 認証フローの設計
  2. 02サーバー側のトークン検証の確認
  3. 03更新・失効の仕組みの計画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mindrally/skills --skill jwt-security

インストール後、Claude Code で/jwt-securityを入力して読み込まれたか確認します。

詳細

JWT セキュリティ

JSON Web Token(JWT)のセキュリティ実装に関するエキスパートです。認証と認可で JWT を扱う際は、次のガイドラインに従ってください。

基本原則

  • JWT は本質的に安全なものではありません。セキュリティは実装に依存します
  • 内部サービスであっても、必ずサーバー側でトークンを検証してください
  • 可能な場合は、非対称署名(RS256、ES256)を使用してください
  • トークンの有効期間を短くし、適切な更新の仕組みを実装してください
  • JWT のペイロードに機密データを保存しないでください

トークン構造

JWT は、ヘッダー、ペイロード、署名の 3 つの部分で構成されます。

header.payload.signature
ヘッダーのベストプラクティス
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "key-identifier-for-rotation"
}
  • キーのローテーションに対応するため、必ず kid(キー ID)を含めてください
  • typ: "JWT" を明示的に指定してください
  • alg: "none" を決して受け入れないでください
ペイロードのベストプラクティス
{
  "iss": "https://auth.example.com",
  "sub": "user-uuid-here",
  "aud": "https://api.example.com",
  "exp": 1704067200,
  "iat": 1704063600,
  "nbf": 1704063600,
  "jti": "unique-token-id"
}

必須のクレーム:

  • iss(発行者):トークンを作成した者
  • sub(サブジェクト):トークンが表す対象
  • aud(オーディエンス):トークンの対象となる相手
  • exp(有効期限):トークンの有効期限が切れる時刻
  • iat(発行時刻):トークンが作成された時刻

推奨されるクレーム:

  • nbf(有効開始時刻):この時刻より前はトークンが無効
  • jti(JWT ID):トークン失効に使用する一意の識別子

署名アルゴリズムの選択

推奨:非対称アルゴリズム
// RS256 - RSA with SHA-256 (most widely supported)
// ES256 - ECDSA with P-256 and SHA-256 (smaller keys)
// EdDSA - Edwards-curve Digital Signature Algorithm (most secure)

const ALLOWED_ALGORITHMS = ['RS256', 'ES256', 'EdDSA'];
対称アルゴリズムが必要な場合
// HS256 - HMAC with SHA-256
// Only use with a strong secret (minimum 256 bits / 32 bytes)
const secret = crypto.randomBytes(64).toString('hex');

トークンの作成

RS256 を使用する(推奨)
const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('private.pem');

function createToken(userId, roles) {
  const payload = {
    sub: userId,
    roles: roles,
    // Keep custom claims minimal
  };

  const options = {
    algorithm: 'RS256',
    expiresIn: '15m', // Short-lived access tokens
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
    keyid: 'current-key-id',
  };

  return jwt.sign(payload, privateKey, options);
}
トークンの有効期間に関するガイドライン
const TOKEN_LIFETIMES = {
  accessToken: '15m',      // 15 minutes max
  refreshToken: '7d',      // 7 days with rotation
  idToken: '1h',           // 1 hour
  passwordReset: '15m',    // 15 minutes
  emailVerification: '24h', // 24 hours
};

トークンの検証

検証の完全な例
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

// JWKS client for fetching public keys
const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  cacheMaxAge: 600000, // 10 minutes
  rateLimit: true,
  jwksRequestsPerMinute: 10,
});

async function validateToken(token) {
  // 1. Decode header without verification to get kid
  const decoded = jwt.decode(token, { complete: true });

  if (!decoded) {
    throw new Error('Invalid token format');
  }

  // 2. Validate algorithm against whitelist
  if (!ALLOWED_ALGORITHMS.includes(decoded.header.alg)) {
    throw new Error(`Algorithm ${decoded.header.alg} not allowed`);
  }

  // 3. Get signing key
  const key = await client.getSigningKey(decoded.header.kid);
  const publicKey = key.getPublicKey();

  // 4. Verify signature and claims
  const verified = jwt.verify(token, publicKey, {
    algorithms: ALLOWED_ALGORITHMS, // Whitelist algorithms
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
    clockTolerance: 30, // 30 seconds clock skew tolerance
  });

  return verified;
}
検証チェックリスト
function validateTokenClaims(decoded) {
  const now = Math.floor(Date.now() / 1000);

  // 1. Check expiration
  if (decoded.exp && decoded.exp < now) {
    throw new Error('Token expired');
  }

  // 2. Check not before
  if (decoded.nbf && decoded.nbf > now) {
    throw new Error('Token not yet valid');
  }

  // 3. Check issuer
  if (decoded.iss !== EXPECTED_ISSUER) {
    throw new Error('Invalid issuer');
  }

  // 4. Check audience
  const audiences = Array.isArray(decoded.aud) ? decoded.aud : [decoded.aud];
  if (!audiences.includes(EXPECTED_AUDIENCE)) {
    throw new Error('Invalid audience');
  }

  // 5. Check required claims exist
  if (!decoded.sub) {
    throw new Error('Missing subject claim');
  }

  return true;
}

防止すべきセキュリティ脆弱性

1. アルゴリズム混同攻撃
// WRONG: Accepting any algorithm
jwt.verify(token, secret); // Vulnerable!

// CORRECT: Whitelist allowed algorithms
jwt.verify(token, key, { algorithms: ['RS256'] });
2. None アルゴリズム攻撃
// Always reject 'none' algorithm
if (decoded.header.alg === 'none' || decoded.header.alg.toLowerCase() === 'none') {
  throw new Error('Algorithm none is not allowed');
}
3. キー混同(RS256 と HS256)
// When using asymmetric keys, never allow symmetric algorithms
const ASYMMETRIC_ONLY = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'EdDSA'];

jwt.verify(token, publicKey, { algorithms: ASYMMETRIC_ONLY });
4. 弱い HMAC シークレット
// Minimum 256-bit (32 byte) secret for HS256
// Minimum 384-bit (48 byte) secret for HS384
// Minimum 512-bit (64 byte) secret for HS512

function generateHmacSecret(algorithm) {
  const bits = parseInt(algorithm.slice(2)); // HS256 -> 256
  const bytes = bits / 8;
  return crypto.randomBytes(Math.max(bytes, 32)).toString('hex');
}

トークンの保存

ブラウザーでの保存に関するセキュリティ
// Best: HttpOnly cookie (requires backend support)
// Server sets:
res.cookie('access_token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 900000, // 15 minutes
});

// Acceptable: In-memory (lost on refresh)
let accessToken = null;
function setToken(token) {
  accessToken = token;
}

// Avoid: localStorage (vulnerable to XSS)
// Avoid: sessionStorage for sensitive tokens
トークンの送信
// Always use Authorization header
fetch('/api/resource', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

// Never put tokens in URLs (logged, cached, visible in history)
// WRONG: /api/resource?token=eyJ...

リフレッシュトークンの実装

// Refresh tokens should be:
// 1. Stored securely (httpOnly cookie or secure server-side storage)
// 2. Rotated on each use
// 3. Bound to the client (if possible)

async function refreshAccessToken(refreshToken) {
  // Validate refresh token
  const decoded = await validateRefreshToken(refreshToken);

  // Check if token has been revoked
  const isRevoked = await checkTokenRevocation(decoded.jti);
  if (isRevoked) {
    throw new Error('Refresh token has been revoked');
  }

  // Generate new tokens
  const newAccessToken = createAccessToken(decoded.sub);
  const newRefreshToken = createRefreshToken(decoded.sub);

  // Revoke old refresh token (rotation)
  await revokeToken(decoded.jti);

  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}

トークンの失効

// Maintain a revocation list for early token invalidation
const revokedTokens = new Set(); // Use Redis in production

function revokeToken(jti) {
  revokedTokens.add(jti);
}

function isTokenRevoked(jti) {
  return revokedTokens.has(jti);
}

// Include revocation check in validation
async function validateToken(token) {
  const decoded = jwt.verify(token, key, options);

  if (decoded.jti && isTokenRevoked(decoded.jti)) {
    throw new Error('Token has been revoked');
  }

  return decoded;
}

キーのローテーション

// Support multiple keys during rotation
const keyStore = {
  'key-2024-01': { /* current key */ },
  'key-2023-12': { /* previous key, still valid */ },
};

// JWKS endpoint should expose all valid public keys
app.get('/.well-known/jwks.json', (req, res) => {
  const keys = Object.entries(keyStore).map(([kid, key]) => ({
    kid,
    kty: 'RSA',
    use: 'sig',
    alg: 'RS256',
    n: key.publicKey.n,
    e: key.publicKey.e,
  }));

  res.json({ keys });
});

Express ミドルウェアの例

const expressJwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');

const jwtMiddleware = expressJwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  }),
  audience: 'https://api.example.com',
  issuer: 'https://auth.example.com',
  algorithms: ['RS256'],
});

// Protected route
app.get('/api/protected', jwtMiddleware, (req, res) => {
  // req.auth contains the decoded token
  res.json({ user: req.auth.sub });
});

テスト

describe('JWT Validation', () => {
  it('should reject expired tokens', async () => {
    const expiredToken = createToken({ exp: Math.floor(Date.now() / 1000) - 3600 });
    await expect(validateToken(expiredToken)).rejects.toThrow('expired');
  });

  it('should reject tokens with wrong issuer', async () => {
    const wrongIssuer = createToken({ iss: 'https://evil.com' });
    await expect(validateToken(wrongIssuer)).rejects.toThrow('issuer');
  });

  it('should reject none algorithm', async () => {
    const noneAlg = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.';
    await expect(validateToken(noneAlg)).rejects.toThrow('algorithm');
  });
});

避けるべき一般的なアンチパターン

  1. セッション管理に JWT を使用する(ウェブアプリではサーバー側のセッションを推奨します)
  2. JWT のペイロードに機密データを保存する(暗号化されているのではなく、エンコードされているだけです)
  3. すべてのクレームを検証しない
  4. 弱い、またはハードコードされたシークレットを使用する
  5. トークンの有効期限を実装しない
  6. 検証せずにアルゴリズムヘッダーを信頼する
  7. リフレッシュトークンのローテーションを実装しない
  8. トークン全体をログに記録する

コメント