Extract shared auth logic and validation rules to shared/auth.ts Introduce utility functions for HTTP errors and user input parsing Standardize error messages and date formatting across the app
67 lines
2.2 KiB
TypeScript
67 lines
2.2 KiB
TypeScript
import { verifyAuthenticationResponse, type AuthenticationResponseJSON } from '@simplewebauthn/server'
|
|
|
|
import { signInUser } from '../../../../utils/auth'
|
|
import { assertBadRequest, httpError } from '../../../../utils/http'
|
|
import { getCredentialForVerification, updateCredentialCounter } from '../../../../utils/user-repository'
|
|
import { requireExistingUser } from '../../../../utils/users'
|
|
import { consumeLoginChallenge, getWebAuthnConfig, toWebAuthnCredential } from '../../../../utils/webauthn'
|
|
|
|
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
|
|
|
|
assertBadRequest(response, 'Passkey login payload is incomplete')
|
|
assertBadRequest(challengeToken, 'Passkey login payload is incomplete')
|
|
|
|
const expectedChallenge = await consumeLoginChallenge(challengeToken)
|
|
|
|
if (!expectedChallenge) {
|
|
httpError(400, 'Passkey login challenge expired. Try again.')
|
|
}
|
|
|
|
const storedCredential = await getCredentialForVerification(response.id)
|
|
|
|
if (!storedCredential) {
|
|
httpError(401, '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) {
|
|
httpError(401, 'Passkey authentication failed')
|
|
}
|
|
|
|
const user = await requireExistingUser(storedCredential.userId, 'User account is not available')
|
|
|
|
if (!user.isActive) {
|
|
httpError(401, '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
|
|
}
|
|
})
|