mcpField
A
authentication-patterns

authentication-patterns

github.com/travisjneuman安全106141 次安装1 天前更新
代码仓库

关于 authentication-patterns

authentication-patterns 是面向开发者的技能,帮助在 Web 和移动应用中实现身份验证与授权。内容涵盖 OAuth、令牌、多因素验证、单点登录、会话和访问控制,并提供安全原则与实现示例。

主要功能

  1. 01OAuth 2.0 与 PKCE 流程
  2. 02JWT 与会话管理
  3. 03TOTP 和 WebAuthn 多因素验证
  4. 04基于 SAML 和 OIDC 的单点登录
  5. 05RBAC 与 ABAC 授权
  6. 06安全检查清单与反模式

适用场景

  1. 01添加社交登录或注册流程
  2. 02通过令牌轮换保护 API 访问
  3. 03为敏感账户启用多因素验证
  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. 令牌就是凭据 - 访问令牌、刷新令牌和会话 Cookie 必须安全存储(httpOnly Cookie、加密存储),仅通过 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),无需使用短信(短信易受 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,迭代次数:3最能防御 GPU/ASIC 攻击
bcrypt良好轮数:12-14支持广泛,经过验证
scrypt良好N:2^15,r:8,p:1内存密集型,是 argon2 的良好替代方案
PBKDF2可接受迭代次数:600,000+(SHA-256)NIST 推荐,但不是内存密集型
MD5/SHA-256绝不可用不适用不是密码哈希算法——速度太快,且没有盐值

反模式

反模式问题所在更好的做法
以明文存储密码一次泄露就会暴露所有密码使用 argon2id 或 bcrypt,并为每位用户使用独立盐值
将 JWT 存储在 localStorage 中XSS 攻击可能窃取令牌使用 httpOnly Secure Cookie
访问令牌有效期过长(数天/数周)令牌被盗后,造成的损失会持续更久使用 15 分钟有效期的访问令牌,并轮换刷新令牌
只在客户端检查权限客户端检查可以被绕过使用服务端中间件,并在客户端提供相应体验
自行实现 OAuth容易出现细微的安全漏洞(state、PKCE、重定向)使用成熟的库(Auth.js、Passport)
将基于 SMS 的 2FA 作为唯一 MFA 选项存在 SIM 卡交换攻击风险除 SMS 外,还提供 TOTP 和 WebAuthn
登录端点没有速率限制容易遭受暴力破解攻击按 IP 和用户名限制速率
密码重置令牌永不过期令牌可以无限期使用设置 1 小时有效期、仅可使用一次,并在密码更改时使其失效

检查清单

  • 使用 argon2id 或 bcrypt 哈希密码(绝不使用明文、MD5 或 SHA)
  • 公共客户端(SPA、移动应用)的 OAuth 流程使用 PKCE
  • 访问令牌的有效期较短(< 30 分钟)
  • 刷新令牌在使用时轮换,并存储在服务端
  • 通过 httpOnly Secure SameSite Cookie 传递令牌(Web)
  • 对登录端点进行速率限制(按 IP 和用户名)
  • 所有用户均可使用 MFA,管理员角色必须启用 MFA
  • 在设置 MFA 时生成恢复码
  • 在服务端(中间件)和客户端(用户体验)检查 RBAC 权限
  • 密码重置令牌 1 小时后过期,且仅可使用一次
  • 连续失败 N 次后锁定账户(并逐步延长等待时间)
  • 更改密码时使会话失效

相关资源

  • 技能: application-security(OWASP 身份验证漏洞)、payment-integration(账单身份验证)
  • 技能: email-systems(密码重置邮件)、product-analytics(在身份验证时识别/关联)
  • 规则: docs/reference/stacks/fullstack-nextjs-nestjs.md(NestJS 身份验证守卫)

评论

支持 Markdown