jwt について
Node.js アプリケーションで JWT 認証を実装し、安全に運用するためのリファレンススキルです。トークンの検証やローテーション、失効処理、本番環境のセキュリティ指針を扱います。
主な機能
- 01JWT の構造とクレーム
- 02Node.js のトークン生成とミドルウェア
- 03安全な保存方法とアルゴリズムの指針
- 04リフレッシュトークンのローテーションと失効
- 05トークン関連の一般的な問題の調査
活用シーン
- 01Bearer トークン認証の構築
- 02JWT の保存方法と有効期限の検討
- 03リフレッシュトークンのローテーションと失効処理の追加
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill jwtインストール後、Claude Code で/jwtを入力して読み込まれたか確認します。
同梱ファイルquick-ref/implementation.mdquick-ref/security.md
詳細
JWT コア知識
深い知識:包括的なドキュメントを参照するには、technology:
jwtを指定してmcp__documentation__fetch_docsを使用してください。
トークンの構造
header.payload.signature
Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "1234", "name": "John", "iat": 1516239022 }
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)
Node.js の実装
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET!;
// Generate token
function generateToken(user: User): string {
return jwt.sign(
{ sub: user.id, email: user.email },
SECRET,
{ expiresIn: '1h' }
);
}
// Verify token
function verifyToken(token: string): JwtPayload {
return jwt.verify(token, SECRET) as JwtPayload;
}
// Refresh token pattern
function generateRefreshToken(user: User): string {
return jwt.sign(
{ sub: user.id, type: 'refresh' },
SECRET,
{ expiresIn: '7d' }
);
}
ミドルウェア
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.split(' ')[1];
try {
req.user = verifyToken(token);
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
このスキルを使用しない場合
- セッションベース認証 - Cookie を使用した従来のサーバーサイドセッションを使用してください
- OAuth 2.0 フロー - サードパーティ認証には
oauth2スキルを使用してください - NextAuth.js - Next.js 認証には
nextauthスキルを使用してください - シンプルな内部 API - API キーで十分な場合があります
ベストプラクティス
| 推奨事項 | 禁止事項 |
|---|---|
| HTTPS を使用する | localStorage に保存する(httpOnly Cookie を使用してください) |
| 有効期限を短くする(15m~1h) | ペイロードに機密データを含める |
| すべてのクレームを検証する | 脆弱なシークレットを使用する |
| リフレッシュトークンを使用する | 有効期限を無視する |
アンチパターン
| アンチパターン | 問題点 | 正しい方法 |
|---|---|---|
| JWT を localStorage に保存する | XSS 攻撃に対して脆弱です | httpOnly Cookie を使用する |
| 有効期間の長いアクセストークン | 漏えいした場合にセキュリティリスクがあります | 有効期限を15分にし、リフレッシュトークンを使用する |
| 脆弱なシークレット(< 32 bytes) | ブルートフォース攻撃で簡単に破られます | 256-bit のランダムなシークレットを使用する |
| アルゴリズムの検証を無視する | アルゴリズム混同攻撃が発生する可能性があります | 許可するアルゴリズムを明示的に指定する |
| ペイロードにパスワードを含める | トークンは base64 エンコードされているだけで、暗号化されていません | 機密性のないクレームのみを含める |
| トークン失効の仕組みがない | ユーザーがログアウトできません | ブラックリストまたはトークンのバージョン管理を実装する |
簡単なトラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| "Invalid signature" | シークレットまたはアルゴリズムが誤っています | JWT_SECRET が一致していることを確認し、アルゴリズムを確認してください |
| "Token expired" | exp クレームの日時が過去です | リフレッシュトークンのフローを実装してください |
| "Missing token" | Authorization ヘッダーが送信されていません | Authorization: Bearer <token> を確認してください |
| トークンが認識されない | トークンの形式が不正です | header.payload.signature の形式を確認してください |
| Cookie に関する CORS エラー | SameSite/Secure フラグの設定に問題があります | sameSite:'strict', secure:true を設定してください |
| ログアウトが機能しない | トークンがステートレスです | Redis/DB を使用した失効処理を実装してください |
標準クレーム
| クレーム | 目的 |
|---|---|
sub | サブジェクト(ユーザー ID) |
iat | 発行日時 |
exp | 有効期限 |
iss | 発行者 |
aud | 対象者 |
本番環境への対応
セキュリティ設定
// Use asymmetric keys (RS256) for production
import * as jose from 'jose';
// Generate key pair (run once, store securely)
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem
const privateKey = await jose.importPKCS8(
process.env.JWT_PRIVATE_KEY!,
'RS256'
);
const publicKey = await jose.importSPKI(
process.env.JWT_PUBLIC_KEY!,
'RS256'
);
// Sign token
async function generateToken(user: User): Promise<string> {
return new jose.SignJWT({
sub: user.id,
email: user.email,
})
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
.setIssuedAt()
.setIssuer(process.env.JWT_ISSUER!)
.setAudience(process.env.JWT_AUDIENCE!)
.setExpirationTime('15m') // Short-lived access token
.sign(privateKey);
}
// Verify token
async function verifyToken(token: string): Promise<jose.JWTPayload> {
const { payload } = await jose.jwtVerify(token, publicKey, {
issuer: process.env.JWT_ISSUER!,
audience: process.env.JWT_AUDIENCE!,
});
return payload;
}
安全なトークンの保存
// Server-side: HttpOnly cookie for access token
res.cookie('access_token', token, {
httpOnly: true, // Prevents XSS access
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 15 * 60 * 1000, // 15 minutes
path: '/',
});
// Refresh token in separate cookie
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/api/auth/refresh', // Only sent to refresh endpoint
});
トークンのローテーションと失効
// Refresh token rotation
async function refreshTokens(refreshToken: string) {
// Verify refresh token
const payload = await verifyRefreshToken(refreshToken);
// Check if refresh token is in blacklist (revoked)
if (await isTokenRevoked(refreshToken)) {
throw new Error('Token revoked');
}
// Revoke old refresh token
await revokeToken(refreshToken);
// Generate new tokens
const user = await db.users.findUnique({ where: { id: payload.sub } });
return {
accessToken: await generateToken(user),
refreshToken: await generateRefreshToken(user),
};
}
// Token revocation with Redis
async function revokeToken(token: string): Promise<void> {
const payload = await jose.decodeJwt(token);
const ttl = payload.exp! - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.set(`revoked:${token}`, '1', 'EX', ttl);
}
}
// Logout: revoke all user tokens
async function logoutAll(userId: string): Promise<void> {
// Increment user's token version, invalidating all existing tokens
await db.users.update({
where: { id: userId },
data: { tokenVersion: { increment: 1 } },
});
}
アルゴリズムのセキュリティ
// NEVER allow 'none' algorithm
// ALWAYS specify allowed algorithms explicitly
const { payload } = await jose.jwtVerify(token, publicKey, {
algorithms: ['RS256'], // Only allow RS256
issuer: process.env.JWT_ISSUER!,
audience: process.env.JWT_AUDIENCE!,
});
// Validate token type to prevent token confusion
if (payload.type !== 'access') {
throw new Error('Invalid token type');
}
監視メトリクス
| メトリクス | アラートのしきい値 |
|---|---|
| トークン検証の失敗 | > 100/min |
| リフレッシュトークンの再利用試行 | > 10/min |
| 有効期限切れトークンのリクエスト | > 500/min |
| 署名の無効エラー | > 50/min |
クレームの検証
async function validateTokenClaims(payload: jose.JWTPayload): Promise<void> {
// Check required claims
if (!payload.sub || !payload.iat || !payload.exp) {
throw new Error('Missing required claims');
}
// Check user still exists and is active
const user = await db.users.findUnique({ where: { id: payload.sub } });
if (!user || !user.isActive) {
throw new Error('User not found or inactive');
}
// Check token version (for logout-all functionality)
if (payload.tokenVersion !== user.tokenVersion) {
throw new Error('Token invalidated');
}
}
チェックリスト
- 本番環境では RS256(非対称鍵)を使用する
- アクセストークンの有効期限を短くする(15分)
- ローテーション付きのリフレッシュトークンを使用する
- HttpOnly Cookie を使用する(localStorage は使用しない)
- Cookie に Secure と SameSite のフラグを設定する
- トークン失効の仕組みを用意する
- 発行者と対象者を検証する
- 許可するアルゴリズムを明示的に指定する
- 全デバイスからログアウトするためにトークンバージョンを含める
- トークン検証の失敗を監視する
- トークンエンドポイントにレート制限を適用する
参考ドキュメント
コメント
Markdown 対応