Files
dticket.tootaio.com/server/api/auth/passkey/login/verify.post.ts
xiaomai 377a9617be feat: implement auth system, passkeys, and user management
Add PostgreSQL and Redis integration for users and sessions
Implement password and WebAuthn passkey login flows
Add Docker stack, super-admin seeding, and protected routes
2026-04-12 20:16:43 +08:00

81 lines
2.3 KiB
TypeScript

import { verifyAuthenticationResponse, type AuthenticationResponseJSON } from '@simplewebauthn/server'
import { getUserById, getCredentialForVerification, updateCredentialCounter } from '../../../../utils/user-repository'
import { consumeLoginChallenge, getWebAuthnConfig, toWebAuthnCredential } from '../../../../utils/webauthn'
import { signInUser } from '../../../../utils/auth'
export default defineEventHandler(async (event) => {
const body = await readBody<{
response?: AuthenticationResponseJSON
challengeToken?: string
remember?: boolean
}>(event)
const response = body.response
const challengeToken = body.challengeToken?.trim() || ''
const remember = body.remember !== false
if (!response || !challengeToken) {
throw createError({
statusCode: 400,
statusMessage: 'Passkey login payload is incomplete'
})
}
const expectedChallenge = await consumeLoginChallenge(challengeToken)
if (!expectedChallenge) {
throw createError({
statusCode: 400,
statusMessage: 'Passkey login challenge expired. Try again.'
})
}
const storedCredential = await getCredentialForVerification(response.id)
if (!storedCredential) {
throw createError({
statusCode: 401,
statusMessage: 'Passkey is not recognized'
})
}
const config = getWebAuthnConfig(event)
const verification = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin: config.origin,
expectedRPID: config.rpID,
credential: toWebAuthnCredential(storedCredential)
})
if (!verification.verified) {
throw createError({
statusCode: 401,
statusMessage: 'Passkey authentication failed'
})
}
const user = await getUserById(storedCredential.userId)
if (!user || !user.isActive) {
throw createError({
statusCode: 401,
statusMessage: 'User account is not available'
})
}
await updateCredentialCounter({
credentialId: storedCredential.credentialId,
counter: verification.authenticationInfo.newCounter,
deviceType: verification.authenticationInfo.credentialDeviceType,
backedUp: verification.authenticationInfo.credentialBackedUp
})
const authenticatedUser = await signInUser(event, user, remember)
return {
user: authenticatedUser
}
})