mcpField
W
webauthn

webauthn

github.com/claude-dev-suite安全4661 次安装2 天前更新
代码仓库

关于 webauthn

面向在 Web 应用中实现身份验证的开发者,介绍 WebAuthn 与 Passkey 的实践流程。内容涵盖 SimpleWebAuthn 注册和登录、浏览器调用及生产环境注意事项。

主要功能

  1. 01生成并验证注册选项
  2. 02生成并验证身份验证选项
  3. 03浏览器注册与登录示例
  4. 04挑战值和计数器安全措施
  5. 05凭据管理检查清单

适用场景

  1. 01为 Web 应用添加 Passkey 注册
  2. 02实现带服务端验证的 Passkey 登录
  3. 03检查 WebAuthn 流程的重放风险

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill webauthn

安装后在 Claude Code 中输入/webauthn确认已加载。

详细介绍

WebAuthn / Passkeys

服务器设置(SimpleWebAuthn)

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from '@simplewebauthn/server';

const rpName = 'My App';
const rpID = 'myapp.com';
const origin = 'https://myapp.com';

注册流程

服务器:生成选项
app.post('/api/auth/register/options', auth, async (req, res) => {
  const user = req.user;
  const existingCredentials = await db.credential.findMany({
    where: { userId: user.id },
  });

  const options = await generateRegistrationOptions({
    rpName,
    rpID,
    userName: user.email,
    userDisplayName: user.name,
    excludeCredentials: existingCredentials.map((c) => ({
      id: c.credentialId,
      transports: c.transports,
    })),
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred',
    },
  });

  // Store challenge for verification
  await db.challenge.upsert({
    where: { userId: user.id },
    create: { userId: user.id, challenge: options.challenge },
    update: { challenge: options.challenge },
  });

  res.json(options);
});
服务器:验证注册
app.post('/api/auth/register/verify', auth, async (req, res) => {
  const user = req.user;
  const { challenge } = await db.challenge.findUnique({ where: { userId: user.id } });

  const verification = await verifyRegistrationResponse({
    response: req.body,
    expectedChallenge: challenge,
    expectedOrigin: origin,
    expectedRPID: rpID,
  });

  if (verification.verified && verification.registrationInfo) {
    const { credential } = verification.registrationInfo;
    await db.credential.create({
      data: {
        userId: user.id,
        credentialId: Buffer.from(credential.id),
        publicKey: Buffer.from(credential.publicKey),
        counter: credential.counter,
        transports: req.body.response.transports,
      },
    });
  }

  res.json({ verified: verification.verified });
});

身份验证流程

服务器:生成选项
app.post('/api/auth/login/options', async (req, res) => {
  const options = await generateAuthenticationOptions({
    rpID,
    userVerification: 'preferred',
    // For passkeys (discoverable credentials), omit allowCredentials
  });

  // Store challenge in session
  req.session.challenge = options.challenge;
  res.json(options);
});
服务器:验证身份验证
app.post('/api/auth/login/verify', async (req, res) => {
  const credential = await db.credential.findUnique({
    where: { credentialId: Buffer.from(req.body.id, 'base64url') },
  });

  const verification = await verifyAuthenticationResponse({
    response: req.body,
    expectedChallenge: req.session.challenge,
    expectedOrigin: origin,
    expectedRPID: rpID,
    credential: {
      id: credential.credentialId,
      publicKey: credential.publicKey,
      counter: credential.counter,
    },
  });

  if (verification.verified) {
    // Update counter (replay protection)
    await db.credential.update({
      where: { id: credential.id },
      data: { counter: verification.authenticationInfo.newCounter },
    });

    const token = generateJWT(credential.userId);
    res.json({ verified: true, token });
  }
});

浏览器客户端

import { startRegistration, startAuthentication } from '@simplewebauthn/browser';

// Register
async function registerPasskey() {
  const optionsRes = await fetch('/api/auth/register/options', { method: 'POST' });
  const options = await optionsRes.json();

  const credential = await startRegistration({ optionsJSON: options });

  const verifyRes = await fetch('/api/auth/register/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credential),
  });
  return (await verifyRes.json()).verified;
}

// Login
async function loginWithPasskey() {
  const optionsRes = await fetch('/api/auth/login/options', { method: 'POST' });
  const options = await optionsRes.json();

  const credential = await startAuthentication({ optionsJSON: options });

  const verifyRes = await fetch('/api/auth/login/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credential),
  });
  return (await verifyRes.json()).token;
}

反模式

反模式修复方法
不存储计数器存储并验证计数器,以防止重放攻击
重用质询每次认证流程都生成新的质询
没有备用身份验证方式提供密码或魔法链接作为备用方式
将 rpID 硬编码从环境中配置,且必须与域名匹配
排除项中缺少传输方式存储传输方式,以改善重新注册时的体验

生产环境检查清单

  • 必须使用 HTTPS(WebAuthn 要求安全上下文)
  • 每次认证流程都生成质询,并设置过期时间
  • 存储并验证计数器
  • 支持每位用户拥有多个凭据
  • 凭据管理界面(查看、撤销)
  • 提供备用身份验证方式

评论

支持 Markdown