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 安全性、整合社交登入,以及除錯認證問題的指引。透過運用這些模式,開發人員能夠建置靈活、易於維護且可水平擴展的認證系統。
本技能課程非常適合從事網頁應用程式、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
複製





首頁
