auth-implementation-patterns
amurata/cc-tools
掌握包括JWT、OAuth2、会话管理和RBAC在内的身份验证与授权模式,构建安全且可扩展的访问控制系统。用于身份验证系统的实现、API保护以及安全问题的调试。
...展开全部关于auth-implementation-patterns
auth-implementation-patterns 技能提供了一个全面的框架,用于构建安全且可扩展的身份验证和授权系统。该技能侧重于行业标准模式和最佳实践,使开发人员能够为 Web 应用程序和 API 实现强大的访问控制机制。 该技能同时涵盖身份验证(验证用户身份)和授权(管理权限),有助于防止未经授权的访问,并确保用户能够安全地与资源进行交互。对于需要高安全性、多租户配置,或需与社交登录提供商及企业单点登录(SSO)系统集成的项目而言,该技能尤为有用。
主要功能包括支持基于会话的身份验证、基于 JWT 令牌的身份验证以及 OAuth2/OpenID Connect 委托。本技能演示了如何生成和验证 JWT、实现刷新令牌流程、管理用户会话以及应用基于角色的访问控制(RBAC)策略。 此外,本技能还提供了关于保护 REST 和 GraphQL API 安全、集成社交登录以及调试身份验证问题的指导。通过运用这些模式,开发人员可以构建灵活、易于维护且可水平扩展的身份验证系统。
本技能课程非常适合从事 Web 应用程序、API 开发或企业平台工作的后端开发人员、安全工程师和软件架构师。常见用例包括:实现用户登录系统、保护 API、添加 OAuth2/社交登录、设计会话管理、迁移身份验证系统、排查身份验证相关问题,以及部署单点登录(SSO)或多租户访问控制。 本课程面向所有需要在现代软件应用中确保安全用户身份验证和精细化授权的人员。
常见问题
何时应使用此技能?
在实现用户身份验证系统、保护API安全、集成社交登录、管理会话或设置基于角色的访问控制时,使用本技能最为合适。
支持哪些身份验证方法?
它涵盖基于会话的身份验证、基于 JWT 令牌的身份验证,以及用于委托身份验证和社交登录的 OAuth2/OpenID Connect 流程。
该技能是否适用于 REST 和 GraphQL API?
是的,提供的模式和示例均适用于保护 REST 和 GraphQL API 的安全。
是否有任何先决条件或限制?
开发者应具备 Express.js 等 Web 框架的基础知识,并熟悉 TypeScript 或 JavaScript。本技能假设存在能够存储和验证令牌的环境,例如用于存储刷新令牌的数据库。
本技能是否支持多租户或企业级 SSO 配置?
是的,它提供了使用行业标准模式实现 SSO 和多租户身份验证场景的指导。
English | 日本語
認証・認可実装パターン
業界標準のパターンと最新のベストプラクティスを使用して、安全でスケーラブルな認証・認可システムを構築します。
このスキルを使用するタイミング
- ユーザー認証システムの実装
- REST または GraphQL API の保護
- OAuth2/ソーシャルログインの追加
- ロールベースアクセス制御(RBAC)の実装
- セッション管理の設計
- 認証システムの移行
- 認証問題のデバッグ
- SSO またはマルチテナンシーの実装
コア概念
1. 認証 vs 認可
認証(Authentication, AuthN):あなたは誰ですか?
- 身元の検証(ユーザー名/パスワード、OAuth、生体認証)
- 資格情報の発行(セッション、トークン)
- ログイン/ログアウトの管理
認可(Authorization, AuthZ):あなたは何ができますか?
- 権限チェック
- ロールベースアクセス制御(RBAC)
- リソース所有権の検証
- ポリシー適用
2. 認証戦略
セッションベース:
- サーバーがセッション状態を保存
- Cookie内のセッションID
- 従来型、シンプル、ステートフル
トークンベース(JWT):
- ステートレス、自己完結型
- 水平スケーリング可能
- クレームを保存可能
OAuth2/OpenID Connect:
- 認証の委譲
- ソーシャルログイン(Google、GitHub)
- エンタープライズSSO
JWT認証
パターン1:JWT実装
// JWT構造:header.payload.signatureimport jwt from 'jsonwebtoken';import { Request, Response, NextFunction } from 'express';interface JWTPayload { userId: string; email: string; role: string; iat: number; exp: number;}// JWTを生成function generateTokens(userId: string, email: string, role: string) { const accessToken = jwt.sign( { userId, email, role }, process.env.JWT_SECRET!, { expiresIn: '15m' } // 短命 ); const refreshToken = jwt.sign( { userId }, process.env.JWT_REFRESH_SECRET!, { expiresIn: '7d' } // 長命 ); return { accessToken, refreshToken };}// JWTを検証function verifyToken(token: string): JWTPayload { try { return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload; } catch (error) { if (error instanceof jwt.TokenExpiredError) { throw new Error('トークンが期限切れです'); } if (error instanceof jwt.JsonWebTokenError) { throw new Error('無効なトークンです'); } throw error; }}// ミドルウェアfunction authenticate(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { return res.status(401).json({ error: 'トークンが提供されていません' }); } const token = authHeader.substring(7); try { const payload = verifyToken(token); req.user = payload; // リクエストにユーザーを付加 next(); } catch (error) { return res.status(401).json({ error: '無効なトークンです' }); }}// 使用例app.get('/api/profile', authenticate, (req, res) => { res.json({ user: req.user });});
パターン2:リフレッシュトークンフロー
interface StoredRefreshToken { token: string; userId: string; expiresAt: Date; createdAt: Date;}class RefreshTokenService { // リフレッシュトークンをデータベースに保存 async storeRefreshToken(userId: string, refreshToken: string) { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); await db.refreshTokens.create({ token: await hash(refreshToken), // 保存前にハッシュ化 userId, expiresAt, }); } // アクセストークンをリフレッシュ async refreshAccessToken(refreshToken: string) { // リフレッシュトークンを検証 let payload; try { payload = jwt.verify( refreshToken, process.env.JWT_REFRESH_SECRET! ) as { userId: string }; } catch { throw new Error('無効なリフレッシュトークンです'); } // データベースにトークンが存在するか確認 const storedToken = await db.refreshTokens.findOne({ where: { token: await hash(refreshToken), userId: payload.userId, expiresAt: { $gt: new Date() }, }, }); if (!storedToken) { throw new Error('リフレッシュトークンが見つからないか期限切れです'); } // ユーザーを取得 const user = await db.users.findById(payload.userId); if (!user) { throw new Error('ユーザーが見つかりません'); } // 新しいアクセストークンを生成 const accessToken = jwt.sign( { userId: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET!, { expiresIn: '15m' } ); return { accessToken }; } // リフレッシュトークンを無効化(ログアウト) async revokeRefreshToken(refreshToken: string) { await db.refreshTokens.deleteOne({ token: await hash(refreshToken), }); } // すべてのユーザートークンを無効化(全デバイスからログアウト) async revokeAllUserTokens(userId: string) { await db.refreshTokens.deleteMany({ userId }); }}// APIエンドポイントapp.post('/api/auth/refresh', async (req, res) => { const { refreshToken } = req.body; try { const { accessToken } = await refreshTokenService .refreshAccessToken(refreshToken); res.json({ accessToken }); } catch (error) { res.status(401).json({ error: '無効なリフレッシュトークンです' }); }});app.post('/api/auth/logout', authenticate, async (req, res) => { const { refreshToken } = req.body; await refreshTokenService.revokeRefreshToken(refreshToken); res.json({ message: 'ログアウトしました' });});
セッションベース認証
パターン1:Express Session
import session from 'express-session';import RedisStore from 'connect-redis';import { createClient } from 'redis';// セッションストレージ用にRedisをセットアップconst redisClient = createClient({ url: process.env.REDIS_URL,});await redisClient.connect();app.use( session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET!, resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', // HTTPSのみ httpOnly: true, // JavaScriptからのアクセス不可 maxAge: 24 * 60 * 60 * 1000, // 24時間 sameSite: 'strict', // CSRF保護 }, }));// ログインapp.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; const user = await db.users.findOne({ email }); if (!user || !(await verifyPassword(password, user.passwordHash))) { return res.status(401).json({ error: '無効な認証情報です' }); } // セッションにユーザーを保存 req.session.userId = user.id; req.session.role = user.role; res.json({ user: { id: user.id, email: user.email, role: user.role } });});// セッションミドルウェアfunction requireAuth(req: Request, res: Response, next: NextFunction) { if (!req.session.userId) { return res.status(401).json({ error: '認証されていません' }); } next();}// 保護されたルートapp.get('/api/profile', requireAuth, async (req, res) => { const user = await db.users.findById(req.session.userId); res.json({ user });});// ログアウトapp.post('/api/auth/logout', (req, res) => { req.session.destroy((err) => { if (err) { return res.status(500).json({ error: 'ログアウトに失敗しました' }); } res.clearCookie('connect.sid'); res.json({ message: 'ログアウトしました' }); });});
OAuth2 / ソーシャルログイン
パターン1:Passport.jsを使用したOAuth2
import passport from 'passport';import { Strategy as GoogleStrategy } from 'passport-google-oauth20';import { Strategy as GitHubStrategy } from 'passport-github2';// Google OAuthpassport.use( new GoogleStrategy( { clientID: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, callbackURL: '/api/auth/google/callback', }, async (accessToken, refreshToken, profile, done) => { try { // ユーザーを検索または作成 let user = await db.users.findOne({ googleId: profile.id, }); if (!user) { user = await db.users.create({ googleId: profile.id, email: profile.emails?.[0]?.value, name: profile.displayName, avatar: profile.photos?.[0]?.value, }); } return done(null, user); } catch (error) { return done(error, undefined); } } ));// ルートapp.get('/api/auth/google', passport.authenticate('google', { scope: ['profile', 'email'],}));app.get( '/api/auth/google/callback', passport.authenticate('google', { session: false }), (req, res) => { // JWTを生成 const tokens = generateTokens(req.user.id, req.user.email, req.user.role); // トークンと共にフロントエンドにリダイレクト res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`); });
認可パターン
パターン1:ロールベースアクセス制御(RBAC)
enum Role { USER = 'user', MODERATOR = 'moderator', ADMIN = 'admin',}const roleHierarchy: Record<Role, Role[]> = { [Role.ADMIN]: [Role.ADMIN, Role.MODERATOR, Role.USER], [Role.MODERATOR]: [Role.MODERATOR, Role.USER], [Role.USER]: [Role.USER],};function hasRole(userRole: Role, requiredRole: Role): boolean { return roleHierarchy[userRole].includes(requiredRole);}// ミドルウェアfunction requireRole(...roles: Role[]) { return (req: Request, res: Response, next: NextFunction) => { if (!req.user) { return res.status(401).json({ error: '認証されていません' }); } if (!roles.some(role => hasRole(req.user.role, role))) { return res.status(403).json({ error: '権限が不十分です' }); } next(); };}// 使用例app.delete('/api/users/:id', authenticate, requireRole(Role.ADMIN), async (req, res) => { // 管理者のみがユーザーを削除可能 await db.users.delete(req.params.id); res.json({ message: 'ユーザーを削除しました' }); });
パターン2:権限ベースアクセス制御
enum Permission { READ_USERS = 'read:users', WRITE_USERS = 'write:users', DELETE_USERS = 'delete:users', READ_POSTS = 'read:posts', WRITE_POSTS = 'write:posts',}const rolePermissions: Record<Role, Permission[]> = { [Role.USER]: [Permission.READ_POSTS, Permission.WRITE_POSTS], [Role.MODERATOR]: [ Permission.READ_POSTS, Permission.WRITE_POSTS, Permission.READ_USERS, ], [Role.ADMIN]: Object.values(Permission),};function hasPermission(userRole: Role, permission: Permission): boolean { return rolePermissions[userRole]?.includes(permission) ?? false;}function requirePermission(...permissions: Permission[]) { return (req: Request, res: Response, next: NextFunction) => { if (!req.user) { return res.status(401).json({ error: '認証されていません' }); } const hasAllPermissions = permissions.every(permission => hasPermission(req.user.role, permission) ); if (!hasAllPermissions) { return res.status(403).json({ error: '権限が不十分です' }); } next(); };}// 使用例app.get('/api/users', authenticate, requirePermission(Permission.READ_USERS), async (req, res) => { const users = await db.users.findAll(); res.json({ users }); });
パターン3:リソース所有権
// ユーザーがリソースを所有しているか確認async function requireOwnership( resourceType: 'post' | 'comment', resourceIdParam: string = 'id') { return async (req: Request, res: Response, next: NextFunction) => { if (!req.user) { return res.status(401).json({ error: '認証されていません' }); } const resourceId = req.params[resourceIdParam]; // 管理者はすべてにアクセス可能 if (req.user.role === Role.ADMIN) { return next(); } // 所有権を確認 let resource; if (resourceType === 'post') { resource = await db.posts.findById(resourceId); } else if (resourceType === 'comment') { resource = await db.comments.findById(resourceId); } if (!resource) { return res.status(404).json({ error: 'リソースが見つかりません' }); } if (resource.userId !== req.user.userId) { return res.status(403).json({ error: '権限がありません' }); } next(); };}// 使用例app.put('/api/posts/:id', authenticate, requireOwnership('post'), async (req, res) => { // ユーザーは自分の投稿のみ更新可能 const post = await db.posts.update(req.params.id, req.body); res.json({ post }); });
セキュリティベストプラクティス
パターン1:パスワードセキュリティ
import bcrypt from 'bcrypt';import { z } from 'zod';// パスワード検証スキーマconst passwordSchema = z.string() .min(12, 'パスワードは12文字以上である必要があります') .regex(/[A-Z]/, 'パスワードには大文字を含める必要があります') .regex(/[a-z]/, 'パスワードには小文字を含める必要があります') .regex(/[0-9]/, 'パスワードには数字を含める必要があります') .regex(/[^A-Za-z0-9]/, 'パスワードには特殊文字を含める必要があります');// パスワードをハッシュ化async function hashPassword(password: string): Promise<string> { const saltRounds = 12; // 2^12回の反復 return bcrypt.hash(password, saltRounds);}// パスワードを検証async function verifyPassword( password: string, hash: string): Promise<boolean> { return bcrypt.compare(password, hash);}// パスワード検証付き登録app.post('/api/auth/register', async (req, res) => { try { const { email, password } = req.body; // パスワードを検証 passwordSchema.parse(password); // ユーザーが存在するか確認 const existingUser = await db.users.findOne({ email }); if (existingUser) { return res.status(400).json({ error: 'メールアドレスは既に登録されています' }); } // パスワードをハッシュ化 const passwordHash = await hashPassword(password); // ユーザーを作成 const user = await db.users.create({ email, passwordHash, }); // トークンを生成 const tokens = generateTokens(user.id, user.email, user.role); res.status(201).json({ user: { id: user.id, email: user.email }, ...tokens, }); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors[0].message }); } res.status(500).json({ error: '登録に失敗しました' }); }});
パターン2:レート制限
import rateLimit from 'express-rate-limit';import RedisStore from 'rate-limit-redis';// ログインレート制限const loginLimiter = rateLimit({ store: new RedisStore({ client: redisClient }), windowMs: 15 * 60 * 1000, // 15分 max: 5, // 5回の試行 message: 'ログイン試行回数が多すぎます。後でもう一度お試しください', standardHeaders: true, legacyHeaders: false,});// APIレート制限const apiLimiter = rateLimit({ windowMs: 60 * 1000, // 1分 max: 100, // 1分あたり100リクエスト standardHeaders: true,});// ルートに適用app.post('/api/auth/login', loginLimiter, async (req, res) => { // ログインロジック});app.use('/api/', apiLimiter);
ベストプラクティス
- 平文パスワードを保存しない:常にbcrypt/argon2でハッシュ化
- HTTPSを使用:転送中のデータを暗号化
- 短命のアクセストークン:最大15〜30分
- 安全なCookie:httpOnly、secure、sameSiteフラグ
- すべての入力を検証:メール形式、パスワード強度
- 認証エンドポイントにレート制限:ブルートフォース攻撃を防止
- CSRF保護を実装:セッションベース認証用
- シークレットを定期的にローテーション:JWTシークレット、セッションシークレット
- セキュリティイベントをログ記録:ログイン試行、認証失敗
- 可能な限りMFAを使用:追加のセキュリティ層
よくある落とし穴
- 弱いパスワード:強力なパスワードポリシーを適用
- localStorageにJWT:XSSに脆弱、httpOnly Cookieを使用
- トークン有効期限なし:トークンは期限切れにすべき
- クライアント側の認証チェックのみ:常にサーバー側で検証
- 安全でないパスワードリセット:有効期限付きの安全なトークンを使用
- レート制限なし:ブルートフォース攻撃に脆弱
- クライアントデータを信頼:常にサーバーで検証
リソース
- references/jwt-best-practices.md:JWT実装ガイド
- references/oauth2-flows.md:OAuth2フロー図と例
- references/session-security.md:安全なセッション管理
- assets/auth-security-checklist.md:セキュリティレビューチェックリスト
- assets/password-policy-template.md:パスワード要件テンプレート
- scripts/token-validator.ts:JWT検証ユーティリティ
安装 auth-implementation-patterns
下载技能文件并将其解压到 .claude/skills/ 目录中。
下载ZIP克隆仓库并复制技能文件到您的项目中。
git clone https://github.com/amurata/cc-tools/blob/main/i18n/ja/plugins/developer-essentials/skills/auth-implementation-patterns/SKILL.md # Copy SKILL.md to your .claude/skills/ directory
复制





首页
