mcpField
A
authentication-patterns

authentication-patterns

github.com/travisjneumanセキュリティ106141 回インストール今日更新
リポジトリ

authentication-patterns について

authentication-patterns は、Web・モバイルアプリの認証と認可を実装する開発者向けスキルです。OAuth、トークン、多要素認証、SSO、セッション、アクセス制御の手法を、安全原則や実装例とともに扱います。

主な機能

  1. 01OAuth 2.0 と PKCE のフロー
  2. 02JWT とセッション管理
  3. 03TOTP と WebAuthn による MFA
  4. 04SAML と OIDC による SSO
  5. 05RBAC と ABAC による認可
  6. 06セキュリティチェックリストとアンチパターン

活用シーン

  1. 01ソーシャルログインや登録フローの追加
  2. 02トークンローテーションによる API アクセス保護
  3. 03機密性の高いアカウントへの MFA 導入
  4. 04アプリ内リソースの権限定義

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/travisjneuman/.claude --skill authentication-patterns

インストール後、Claude Code で/authentication-patternsを入力して読み込まれたか確認します。

詳細

認証パターン

概要

このスキルでは、Web アプリケーションとモバイルアプリケーションにおける認証および認可の実装を扱います。OAuth 2.0 フロー(Authorization Code with PKCE、Client Credentials)、JWT 管理(アクセストークン、リフレッシュトークン、ローテーション)、セッション管理戦略、多要素認証(TOTP、WebAuthn/パスキー)、認証ライブラリ(NextAuth/Auth.js v5、Clerk、Supabase Auth、Lucia)との連携、SSO プロトコル(SAML、OIDC)、認可パターン(RBAC、ABAC)を取り上げます。

ログイン/登録フローの構築、ソーシャルログインプロバイダーとの連携、MFA の実装、エンタープライズ顧客向けの SSO の設定、認可モデルの設計、認証プロバイダー間の移行を行う際に、このスキルを使用してください。


基本原則

  1. 暗号処理を独自に実装しない - パスワードのハッシュ化(bcrypt、argon2)、JWT の署名、OAuth フローには、実績のあるライブラリを使用してください。カスタム認証コードは、Web アプリケーションにおけるセキュリティ脆弱性の最大の原因です。
  2. 多層防御 - 認証は単一のチェックではありません。セッション検証、CSRF 保護、レート制限、異常検知を重ねてください。各層は個別に回避される可能性があると想定してください。
  3. トークンは認証情報です - アクセストークン、リフレッシュトークン、セッションクッキーは安全に保存し(httpOnly クッキー、暗号化ストレージ)、HTTPS 経由でのみ送信し、定期的にローテーションする必要があります。
  4. デフォルトで最小権限にする - ユーザーと API クライアントには、最小限の権限から付与してください。アクセス権は明示的なロール割り当てによって昇格させ、暗黙の信頼によって付与してはいけません。
  5. アカウント復旧を計画する - パスワードリセット、MFA 復旧コード、メールアドレスの確認、アカウントロックアウトには、それぞれフローの設計が必要です。これらは通常のログインフローより複雑です。

主なパターン

パターン 1:OAuth とデータベースセッションを使用する NextAuth(Auth.js v5)

使用する場面: ソーシャルログイン、メールアドレス/パスワード認証、またはマジックリンク認証とサーバーサイドのセッション管理が必要な Next.js アプリケーション。

実装:

// auth.ts - Auth.js v5 configuration
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import { verifyPassword } from "@/lib/password";

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Google({
      clientId: process.env.GOOGLE_ID!,
      clientSecret: process.env.GOOGLE_SECRET!,
    }),
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) return null;

        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        });

        if (!user?.passwordHash) return null;

        const valid = await verifyPassword(
          credentials.password as string,
          user.passwordHash
        );

        if (!valid) return null;

        return { id: user.id, email: user.email, name: user.name };
      },
    }),
  ],
  session: {
    strategy: "database", // Server-side sessions (not JWT)
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60, // Refresh session every 24 hours
  },
  callbacks: {
    async session({ session, user }) {
      // Add user role to session
      session.user.id = user.id;
      session.user.role = user.role;
      return session;
    },
    async signIn({ user, account }) {
      // Block sign-in for disabled accounts
      if (user.id) {
        const dbUser = await prisma.user.findUnique({
          where: { id: user.id },
        });
        if (dbUser?.disabled) return false;
      }
      return true;
    },
  },
  pages: {
    signIn: "/login",
    error: "/auth/error",
    verifyRequest: "/auth/verify",
  },
});
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
// Middleware for route protection
// middleware.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";

export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isAuthPage = req.nextUrl.pathname.startsWith("/login") ||
                     req.nextUrl.pathname.startsWith("/register");
  const isDashboard = req.nextUrl.pathname.startsWith("/dashboard");

  if (isDashboard && !isLoggedIn) {
    return NextResponse.redirect(new URL("/login", req.url));
  }

  if (isAuthPage && isLoggedIn) {
    return NextResponse.redirect(new URL("/dashboard", req.url));
  }

  return NextResponse.next();
});

export const config = {
  matcher: ["/dashboard/:path*", "/login", "/register"],
};

理由: Auth.js v5 は、OAuth の複雑な処理(state パラメーター、PKCE、トークン交換)、セッション管理、CSRF 保護、プロバイダー固有の仕様への対応を担います。データベースセッションは、即座に失効でき、クライアントにクレームを公開しないため、JWT セッションより安全です。


パターン 2:JWT アクセストークン/リフレッシュトークンパターン

使用する場面: ステートレスな検証が必要な SPA、モバイルアプリ、またはマイクロサービス間通信における API 認証。

実装:

// Token generation
import jwt from "jsonwebtoken";
import { randomBytes } from "crypto";

interface TokenPayload {
  sub: string; // User ID
  email: string;
  role: string;
}

interface TokenPair {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
}

const ACCESS_TOKEN_EXPIRY = "15m";
const REFRESH_TOKEN_EXPIRY = "7d";

function generateTokenPair(user: TokenPayload): TokenPair {
  const accessToken = jwt.sign(
    { sub: user.sub, email: user.email, role: user.role },
    process.env.JWT_SECRET!,
    {
      expiresIn: ACCESS_TOKEN_EXPIRY,
      issuer: "myapp",
      audience: "myapp-api",
    }
  );

  // Refresh token is opaque (not JWT) - stored server-side
  const refreshToken = randomBytes(64).toString("hex");

  return {
    accessToken,
    refreshToken,
    expiresIn: 900, // 15 minutes in seconds
  };
}

// Token refresh endpoint
async function refreshTokens(refreshToken: string): Promise<TokenPair> {
  // 1. Look up refresh token in database
  const stored = await db.refreshToken.findUnique({
    where: { token: hashToken(refreshToken) },
    include: { user: true },
  });

  if (!stored || stored.expiresAt < new Date()) {
    throw new UnauthorizedError("Invalid or expired refresh token");
  }

  // 2. Rotate refresh token (invalidate old, create new)
  await db.refreshToken.delete({ where: { id: stored.id } });

  const newPair = generateTokenPair({
    sub: stored.user.id,
    email: stored.user.email,
    role: stored.user.role,
  });

  // 3. Store new refresh token
  await db.refreshToken.create({
    data: {
      token: hashToken(newPair.refreshToken),
      userId: stored.user.id,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  });

  return newPair;
}

// Token verification middleware
function verifyAccessToken(token: string): TokenPayload {
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!, {
      issuer: "myapp",
      audience: "myapp-api",
    });
    return decoded as TokenPayload;
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      throw new UnauthorizedError("Access token expired");
    }
    throw new UnauthorizedError("Invalid access token");
  }
}
// Secure cookie-based token delivery (for web apps)
function setAuthCookies(res: Response, tokens: TokenPair): void {
  // Access token in httpOnly cookie
  res.headers.append(
    "Set-Cookie",
    `access_token=${tokens.accessToken}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${tokens.expiresIn}`
  );

  // Refresh token in httpOnly cookie with restricted path
  res.headers.append(
    "Set-Cookie",
    `refresh_token=${tokens.refreshToken}; HttpOnly; Secure; SameSite=Strict; Path=/api/auth/refresh; Max-Age=${7 * 24 * 60 * 60}`
  );
}

理由: 有効期間の短いアクセストークン(15 分)を使うと、トークンが盗まれた場合の被害期間を限定できます。サーバー側に保存する不透明なリフレッシュトークンは、JWT と異なり即座に失効できます。リフレッシュトークンのローテーションにより、トークンの盗難を検知できます。正規のユーザーがすでにローテーションした後に盗まれたリフレッシュトークンが使用された場合、トークンファミリー全体が無効化されます。


パターン 3:多要素認証(TOTP)

使用する場面: パスワードに加えて認証要素が必要な場合。特に、管理者アカウントや機密性の高い操作に適しています。

実装:

// MFA setup flow
import { authenticator } from "otplib";
import QRCode from "qrcode";

// Step 1: Generate secret and QR code for user
async function setupMFA(userId: string): Promise<{ qrCodeUrl: string; secret: string }> {
  const secret = authenticator.generateSecret();

  const user = await db.user.findUniqueOrThrow({ where: { id: userId } });

  // Store encrypted secret (not yet verified)
  await db.mfaSetup.upsert({
    where: { userId },
    create: { userId, secret: encrypt(secret), verified: false },
    update: { secret: encrypt(secret), verified: false },
  });

  const otpauth = authenticator.keyuri(user.email, "MyApp", secret);
  const qrCodeUrl = await QRCode.toDataURL(otpauth);

  return { qrCodeUrl, secret };
}

// Step 2: Verify code to complete setup
async function verifyMFASetup(userId: string, code: string): Promise<string[]> {
  const setup = await db.mfaSetup.findUniqueOrThrow({
    where: { userId },
  });

  const secret = decrypt(setup.secret);
  const isValid = authenticator.verify({ token: code, secret });

  if (!isValid) {
    throw new ValidationError("Invalid verification code");
  }

  // Generate recovery codes
  const recoveryCodes = Array.from({ length: 10 }, () =>
    randomBytes(4).toString("hex").toUpperCase()
  );

  // Store hashed recovery codes
  await db.$transaction([
    db.mfaSetup.update({
      where: { userId },
      data: { verified: true },
    }),
    db.user.update({
      where: { id: userId },
      data: { mfaEnabled: true },
    }),
    ...recoveryCodes.map((code) =>
      db.recoveryCode.create({
        data: { userId, codeHash: hashCode(code) },
      })
    ),
  ]);

  return recoveryCodes; // Show to user ONCE
}

// Step 3: Verify TOTP during login
async function verifyMFA(userId: string, code: string): Promise<boolean> {
  const setup = await db.mfaSetup.findUniqueOrThrow({
    where: { userId, verified: true },
  });

  const secret = decrypt(setup.secret);

  // Check TOTP code (allows 1 window of drift)
  if (authenticator.verify({ token: code, secret })) {
    return true;
  }

  // Check recovery codes
  const recoveryCodes = await db.recoveryCode.findMany({
    where: { userId, used: false },
  });

  for (const rc of recoveryCodes) {
    if (await verifyHash(code, rc.codeHash)) {
      // Mark recovery code as used (one-time use)
      await db.recoveryCode.update({
        where: { id: rc.id },
        data: { used: true, usedAt: new Date() },
      });
      return true;
    }
  }

  return false;
}

理由: TOTP ベースの MFA は広くサポートされており(Google Authenticator、Authy、1Password)、SMS を必要としないため(SMS は SIM スワップの被害を受けやすい)、オフラインでも動作します。復旧コードは、ユーザーが認証アプリのデバイスを失った場合の備えになります。秘密鍵の暗号化と復旧コードのハッシュ化により、データベース侵害への対策ができます。


パターン 4:ロールベースアクセス制御(RBAC)

使用する場面: ユーザーごとにリソースへのアクセスレベルを変える必要がある場合。

実装:

// Permission definitions
const PERMISSIONS = {
  // Projects
  "project:read": "View project details",
  "project:write": "Edit project settings",
  "project:delete": "Delete projects",
  "project:manage_members": "Add/remove project members",

  // Billing
  "billing:read": "View billing information",
  "billing:write": "Manage subscriptions and payments",

  // Admin
  "admin:users": "Manage all users",
  "admin:settings": "Manage organization settings",
} as const;

type Permission = keyof typeof PERMISSIONS;

// Role definitions
const ROLES: Record<string, Permission[]> = {
  viewer: ["project:read"],
  editor: ["project:read", "project:write"],
  admin: [
    "project:read",
    "project:write",
    "project:delete",
    "project:manage_members",
    "billing:read",
    "billing:write",
  ],
  owner: Object.keys(PERMISSIONS) as Permission[],
};

// Permission check middleware
function requirePermission(...requiredPermissions: Permission[]) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const user = req.user;
    if (!user) {
      return res.status(401).json({ error: "Authentication required" });
    }

    const userPermissions = ROLES[user.role] ?? [];
    const hasAllPermissions = requiredPermissions.every((p) =>
      userPermissions.includes(p)
    );

    if (!hasAllPermissions) {
      return res.status(403).json({
        error: "Insufficient permissions",
        required: requiredPermissions,
        current: user.role,
      });
    }

    next();
  };
}

// Usage in routes
app.get("/api/projects/:id", requirePermission("project:read"), getProject);
app.put("/api/projects/:id", requirePermission("project:write"), updateProject);
app.delete("/api/projects/:id", requirePermission("project:delete"), deleteProject);

// Component-level permission checks (React)
function usePermission(permission: Permission): boolean {
  const { data: session } = useSession();
  if (!session?.user?.role) return false;
  const permissions = ROLES[session.user.role] ?? [];
  return permissions.includes(permission);
}

function ProjectSettings({ project }: { project: Project }) {
  const canEdit = usePermission("project:write");
  const canDelete = usePermission("project:delete");

  return (
    <div>
      <h2>Settings</h2>
      {canEdit ? (
        <ProjectForm project={project} />
      ) : (
        <ProjectDetails project={project} />
      )}
      {canDelete && <DeleteProjectButton projectId={project.id} />}
    </div>
  );
}

理由: RBAC は、明確で監査可能な権限モデルを提供します。権限を定数として定義し、TypeScript を使用することで、コンパイル時の安全性を確保できます。サーバー(ミドルウェア)とクライアント(UI)の両方で権限を確認することで、多層防御を実現できます。サーバーはセキュリティを強制し、クライアントはユーザー体験を提供します。


パスワードハッシュ化のリファレンス

アルゴリズム推奨度コスト係数備考
argon2id最適メモリ:64MB、反復回数:3GPU/ASIC 攻撃に対する保護が最も強力です
bcrypt良好ラウンド数:12-14広くサポートされ、実績があります
scrypt良好N:2^15、r:8、p:1メモリ負荷が高く、argon2 の優れた代替手段です
PBKDF2許容範囲反復回数:600,000+(SHA-256)NIST 推奨ですが、メモリ負荷は高くありません
MD5/SHA-256使用不可該当なしパスワードハッシュではありません。処理が速すぎ、ソルトもありません

アンチパターン

アンチパターン問題点より良い方法
パスワードを平文で保存する1 回の侵害ですべてのパスワードが漏えいしますユーザーごとのソルトを使用して argon2id または bcrypt でハッシュ化します
localStorage に JWT を保存するXSS によりトークンが盗まれる可能性がありますhttpOnly Secure Cookie を使用します
有効期間の長いアクセストークン(数日/数週間)盗まれた場合、被害が長期間に及びます15 分間有効なアクセストークンとリフレッシュトークンのローテーションを使用します
クライアント側だけで権限を確認するクライアント側のチェックは回避できますサーバー側のミドルウェアとクライアント側の UX を組み合わせます
OAuth を独自に実装する微妙なセキュリティ上のバグ(state、PKCE、リダイレクト)が発生する可能性があります実績のあるライブラリ(Auth.js、Passport)を使用します
MFA の選択肢を SMS による 2FA のみにするSIM スワッピングの脆弱性がありますSMS と併せて TOTP と WebAuthn を提供します
ログインエンドポイントにレート制限を設定しない総当たり攻撃を受ける可能性がありますIP アドレスとユーザー名に基づいてレート制限を設定します
有効期限のないパスワードリセットトークントークンを無期限に使用できます有効期限を 1 時間にし、1 回限りの使用とし、パスワード変更時に無効化します

チェックリスト

  • パスワードは argon2id または bcrypt でハッシュ化する(平文および MD5/SHA は使用しない)
  • OAuth フローではパブリッククライアント(SPA、モバイル)に PKCE を使用する
  • アクセストークンの有効期間を短くする(30 分未満)
  • リフレッシュトークンは使用時にローテーションし、サーバー側に保存する
  • トークンは httpOnly Secure SameSite Cookie で配信する(Web)
  • ログインエンドポイントにレート制限を設定する(IP アドレスとユーザー名に基づく)
  • すべてのユーザーが MFA を利用できるようにし、管理者ロールには必須とする
  • MFA の設定時にリカバリーコードを生成する
  • RBAC 権限をサーバー(ミドルウェア)とクライアント(UX)で確認する
  • パスワードリセットトークンの有効期限を 1 時間にし、1 回限りの使用とする
  • N 回のログイン失敗後にアカウントをロックする(段階的に遅延を増やす)
  • パスワード変更時にセッションを無効化する

関連リソース

  • スキル: application-security(OWASP 認証の脆弱性)、payment-integration(請求認証)
  • スキル: email-systems(パスワードリセットメール)、product-analytics(認証時の識別/エイリアス)
  • ルール: docs/reference/stacks/fullstack-nextjs-nestjs.md(NestJS 認証ガード)

コメント