关于 webauthn
面向在 Web 应用中实现身份验证的开发者,介绍 WebAuthn 与 Passkey 的实践流程。内容涵盖 SimpleWebAuthn 注册和登录、浏览器调用及生产环境注意事项。
主要功能
- 01生成并验证注册选项
- 02生成并验证身份验证选项
- 03浏览器注册与登录示例
- 04挑战值和计数器安全措施
- 05凭据管理检查清单
适用场景
- 01为 Web 应用添加 Passkey 注册
- 02实现带服务端验证的 Passkey 登录
- 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