jwt-security
JWTを用いた認証を実装する際のセキュリティベストプラクティスを提供するスキルで、トークンの生成・検証・保存に関するガイドラインを網羅します。セキュリティ上のリスクを最小限に抑えた安全なJWT認証フローの構築に役立ちます。
description の原文を見る
Guidelines for implementing JWT authentication with security best practices for token creation, validation, and storage
SKILL.md 本文
JWT Security
JSON Web Token (JWT) のセキュリティ実装の専門家です。認証と認可に JWT を使用する際は、以下のガイドラインに従ってください。
コア原則
- JWT は本質的にセキュアではなく、セキュリティは実装に依存する
- 内部サービスであっても、常にサーバー側でトークンを検証する
- 可能な限り非対称署名(RS256、ES256)を使用する
- トークンは短命にし、適切なリフレッシュメカニズムを実装する
- JWT ペイロードに機密データを保存しない
トークン構造
JWT は Header、Payload、Signature の 3 つの部分で構成されています。
header.payload.signature
Header のベストプラクティス
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-identifier-for-rotation"
}
- キーローテーション対応のため、常に
kid(キー ID)を含める typ: "JWT"を明示的に使用するalg: "none"を絶対に受け入れない
Payload のベストプラクティス
{
"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 (最も広くサポートされている)
// ES256 - ECDSA with P-256 and SHA-256 (キーサイズが小さい)
// EdDSA - Edwards-curve Digital Signature Algorithm (最も安全)
const ALLOWED_ALGORITHMS = ['RS256', 'ES256', 'EdDSA'];
対称署名が必要な場合
// HS256 - HMAC with SHA-256
// 強力なシークレット(最小 256 ビット / 32 バイト)でのみ使用
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,
// カスタムクレームは最小限に
};
const options = {
algorithm: 'RS256',
expiresIn: '15m', // 短命のアクセストークン
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 分
refreshToken: '7d', // 7 日(ローテーション付き)
idToken: '1h', // 1 時間
passwordReset: '15m', // 15 分
emailVerification: '24h', // 24 時間
};
トークン検証
完全な検証の例
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// 公開鍵を取得するための JWKS クライアント
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 600000, // 10 分
rateLimit: true,
jwksRequestsPerMinute: 10,
});
async function validateToken(token) {
// 1. kid を取得するため、署名検証なしでヘッダーをデコード
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
throw new Error('Invalid token format');
}
// 2. アルゴリズムをホワイトリストに対して検証
if (!ALLOWED_ALGORITHMS.includes(decoded.header.alg)) {
throw new Error(`Algorithm ${decoded.header.alg} not allowed`);
}
// 3. 署名鍵を取得
const key = await client.getSigningKey(decoded.header.kid);
const publicKey = key.getPublicKey();
// 4. 署名とクレームを検証
const verified = jwt.verify(token, publicKey, {
algorithms: ALLOWED_ALGORITHMS, // アルゴリズムをホワイトリスト化
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
clockTolerance: 30, // 30 秒のクロックスキュー許容値
});
return verified;
}
検証チェックリスト
function validateTokenClaims(decoded) {
const now = Math.floor(Date.now() / 1000);
// 1. 有効期限をチェック
if (decoded.exp && decoded.exp < now) {
throw new Error('Token expired');
}
// 2. 有効期限前をチェック
if (decoded.nbf && decoded.nbf > now) {
throw new Error('Token not yet valid');
}
// 3. 発行者をチェック
if (decoded.iss !== EXPECTED_ISSUER) {
throw new Error('Invalid issuer');
}
// 4. 対象者をチェック
const audiences = Array.isArray(decoded.aud) ? decoded.aud : [decoded.aud];
if (!audiences.includes(EXPECTED_AUDIENCE)) {
throw new Error('Invalid audience');
}
// 5. 必須クレームの存在をチェック
if (!decoded.sub) {
throw new Error('Missing subject claim');
}
return true;
}
防ぐべきセキュリティ脆弱性
1. アルゴリズム混同攻撃
// 間違い: すべてのアルゴリズムを受け入れる
jwt.verify(token, secret); // 脆弱!
// 正解: 許可されたアルゴリズムをホワイトリスト化
jwt.verify(token, key, { algorithms: ['RS256'] });
2. None アルゴリズム攻撃
// 常に 'none' アルゴリズムを拒否
if (decoded.header.alg === 'none' || decoded.header.alg.toLowerCase() === 'none') {
throw new Error('Algorithm none is not allowed');
}
3. 鍵の混同(RS256 vs HS256)
// 非対称鍵を使用する場合、対称アルゴリズムを許可しない
const ASYMMETRIC_ONLY = ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'EdDSA'];
jwt.verify(token, publicKey, { algorithms: ASYMMETRIC_ONLY });
4. 弱い HMAC シークレット
// HS256 の場合は最小 256 ビット(32 バイト)のシークレット
// HS384 の場合は最小 384 ビット(48 バイト)のシークレット
// HS512 の場合は最小 512 ビット(64 バイト)のシークレット
function generateHmacSecret(algorithm) {
const bits = parseInt(algorithm.slice(2)); // HS256 -> 256
const bytes = bits / 8;
return crypto.randomBytes(Math.max(bytes, 32)).toString('hex');
}
トークンの保存
ブラウザストレージのセキュリティ
// 最適: HttpOnly Cookie(バックエンド対応が必要)
// サーバーが設定:
res.cookie('access_token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 900000, // 15 分
});
// 許容: メモリ内(ページリロード時に消失)
let accessToken = null;
function setToken(token) {
accessToken = token;
}
// 避ける: localStorage(XSS に脆弱)
// 避ける: 機密トークンの sessionStorage
トークンの送信
// 常に Authorization ヘッダーを使用
fetch('/api/resource', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// URL にトークンを入れない(ログ、キャッシュ、履歴に表示される)
// 間違い: /api/resource?token=eyJ...
リフレッシュトークン実装
// リフレッシュトークンは以下であるべき:
// 1. 安全に保存される(httpOnly cookie または安全なサーバー側ストレージ)
// 2. 使用のたびにローテーション
// 3. クライアントにバインドされる(可能な場合)
async function refreshAccessToken(refreshToken) {
// リフレッシュトークンを検証
const decoded = await validateRefreshToken(refreshToken);
// トークンが取り消されているかチェック
const isRevoked = await checkTokenRevocation(decoded.jti);
if (isRevoked) {
throw new Error('Refresh token has been revoked');
}
// 新しいトークンを生成
const newAccessToken = createAccessToken(decoded.sub);
const newRefreshToken = createRefreshToken(decoded.sub);
// 古いリフレッシュトークンを取り消す(ローテーション)
await revokeToken(decoded.jti);
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}
トークン取り消し
// 早期なトークン無効化のための取り消しリストを維持
const revokedTokens = new Set(); // 本番環境では Redis を使用
function revokeToken(jti) {
revokedTokens.add(jti);
}
function isTokenRevoked(jti) {
return revokedTokens.has(jti);
}
// 検証に取り消しチェックを含める
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;
}
鍵ローテーション
// ローテーション中に複数の鍵をサポート
const keyStore = {
'key-2024-01': { /* 現在の鍵 */ },
'key-2023-12': { /* 以前の鍵、まだ有効 */ },
};
// JWKS エンドポイントはすべての有効な公開鍵を公開
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'],
});
// 保護されたルート
app.get('/api/protected', jwtMiddleware, (req, res) => {
// req.auth にデコードされたトークンが含まれる
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');
});
});
避けるべき一般的なアンチパターン
- セッション管理に JWT を使用する(Web アプリではサーバー側セッションを推奨)
- JWT ペイロードに機密データを保存する(エンコードされているだけで暗号化されていない)
- すべてのクレームを検証しない
- 弱い、またはハードコードされたシークレットを使用する
- トークン有効期限を実装しない
- アルゴリズムヘッダーを検証なしで信頼する
- リフレッシュトークンローテーションを実装しない
- 完全なトークンをログに出力する
ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- mindrally
- リポジトリ
- mindrally/skills
- ライセンス
- Apache-2.0
- 最終更新
- 不明
Source: https://github.com/mindrally/skills / ライセンス: Apache-2.0
関連スキル
secure-code-guardian
認証・認可の実装、ユーザー入力の保護、OWASP Top 10の脆弱性対策が必要な場合に使用します。bcrypt/argon2によるパスワードハッシング、パラメータ化ステートメントによるSQLインジェクション対策、CORS/CSPヘッダーの設定、Zodによる入力検証、JWTトークンの構築などのカスタムセキュリティ実装に対応します。認証、認可、入力検証、暗号化、OWASP Top 10対策、セッション管理、セキュリティ強化全般で活用できます。ただし、構築済みのOAuth/SSO統合や単独のセキュリティ監査が必要な場合は、より特化したスキルの検討をお勧めします。
claude-authenticity
APIエンドポイントが本物のClaudeによって支えられているか(ラッパーやプロキシ、偽装ではないか)を、claude-verifyプロジェクトを模した9つの重み付きルールベースチェックで検証できます。また、Claudeの正体を上書きしているプロバイダーから注入されたシステムプロンプトも抽出します。完全に自己完結しており、httpx以外の追加パッケージは不要です。Claude APIキーまたはエンドポイントを検証したい場合、サードパーティのClaudeサービスが本物か確認したい場合、APIプロバイダーのClaude正当性を監査したい場合、複数モデルを並行してテストしたい場合、またはプロバイダーが注入したシステムプロンプトを特定したい場合に使用できます。
anth-security-basics
Anthropic Claude APIのセキュリティベストプラクティスを適用し、キー管理、入力値の検証、プロンプトインジェクション対策を実施します。APIキーの保護、Claudeに送信する前のユーザー入力検証、コンテンツセーフティガードレールの実装が必要な場合に活用できます。「anthropic security」「claude api key security」「secure anthropic」「prompt injection defense」といったフレーズでトリガーされます。
x-ray
x-ray.mdプレ監査レポートを生成します。概要、強化された脅威モデル(プロトコルタイプのプロファイリング、Gitの重み付け攻撃面分析、時間軸リスク分析、コンポーザビリティ依存関係マッピング)、不変条件、統合、ドキュメント品質、テスト分析、開発者・Gitの履歴をカバーしています。「x-ray」「audit readiness」「readiness report」「pre-audit report」「prep this protocol」「protocol prep」「summarize this protocol」のキーワードで実行されます。
semgrep
Semgrepスタティック分析スキャンを実行し、カスタム検出ルールを作成します。Semgrepでのコードスキャン、セキュリティ脆弱性の検出、カスタムYAMLルールの作成、または特定のバグパターンの検出が必要な場合に使用します。重要:ユーザーが「バグをスキャンしたい」「コード品質を確認したい」「脆弱性を見つけたい」「スタティック分析」「セキュリティlint」「コード監査」または「コーディング標準を適用したい」と尋ねた場合も、Semgrepという名称を明記していなくても、このスキルを使用してください。Semgrepは30以上の言語に対応したパターンベースのコードスキャンに最適なツールです。
ghost-bits-cast-attack
Java「ゴーストビッツ」/キャストアタック プレイブック(Black Hat Asia 2026)。16ビット文字が8ビットバイトに暗黙的に縮小されるJavaサービスへの攻撃時に使用します。WAF/IDSを回避して、SQLインジェクション、デシリアライゼーション型RCE、ファイルアップロード(Webシェル)、パストトラバーサル、CRLF インジェクション、リクエストスマグリング、SMTPインジェクションを実行できます。Tomcat、Spring、Jetty、Undertow、Vert.x、Jackson、Fastjson、Apache Commons BCEL、Apache HttpClient、Angus Mail、JDK HttpServer、Lettuce、Jodd、XMLWriterに影響し、WAFバイパスにより多くの「パッチ済み」CVEを再度有効化します。