import { verifyRegistrationResponse } from '@simplewebauthn/server' import { getWebAuthnChallenge, clearWebAuthnChallenge } from '~~/server/utils/webauthn-token' import { getWebAuthnConfig } from '~~/server/utils/webauthn-config' import { db, userIdentities } from '~/drizzle/db' import { createApiError } from '~~/server/utils/apiError' const VALID_TRANSPORTS = ['ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', 'usb'] function sanitizeTransports(transports: unknown): string[] { if (!Array.isArray(transports)) return [] return transports.filter( (t): t is string => typeof t === 'string' && VALID_TRANSPORTS.includes(t) ) } export default defineEventHandler(async (event) => { const user = event.context.user if (!user) { throw createApiError(401, 'AUTH_UNAUTHORIZED_ACCESS', '未授权访问') } const body = await readBody(event) const challengeData = getWebAuthnChallenge(event) if (!challengeData || challengeData.userId !== user.id.toString()) { throw createApiError(400, 'AUTH_CHALLENGE_INVALID', 'Challenge 已失效或不匹配') } // Challenge 只能使用一次,验证失败后也必须重新生成。 clearWebAuthnChallenge(event) const { rpID, origin } = getWebAuthnConfig(event) try { const verification = await verifyRegistrationResponse({ response: body, expectedChallenge: challengeData.challenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: true }) if (verification.verified && verification.registrationInfo) { const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo // 提取凭证数据 const { id: credentialID, publicKey: credentialPublicKey, counter, transports } = credential if (!credentialID || !credentialPublicKey) { throw new Error('注册信息缺少凭证 ID 或公钥') } // 转换为 Base64URL 格式存储 const credentialIDBase64 = credentialID const publicKeyBase64 = Buffer.from(credentialPublicKey).toString('base64url') if (!publicKeyBase64) { throw new Error('生成的公钥为空,注册失败') } // 构造凭证数据 const credentialData = { label: body.label || 'WebAuthn 设备', publicKey: publicKeyBase64, counter: Number(counter), transports: sanitizeTransports(transports), credentialDeviceType, credentialBackedUp } // 存储到数据库 await db.insert(userIdentities).values({ userId: user.id, provider: 'webauthn', providerUserId: credentialIDBase64, providerUsername: JSON.stringify(credentialData), createdAt: new Date() }) return { success: true } } else { throw createApiError(400, 'AUTH_VERIFICATION_FAILED', '验证失败') } } catch (error) { // 透传已携带错误码的业务错误,避免被重新包装后丢失 code 通道。 if (error && typeof error === 'object' && 'statusCode' in error) throw error console.error('WebAuthn 验证失败:', error) const message = error instanceof Error ? error.message : '验证失败' throw createApiError(400, 'AUTH_VERIFICATION_FAILED', message) } })