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
75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
import { verifyRegistrationResponse, type RegistrationResponseJSON } from '@simplewebauthn/server'
|
|
|
|
import { requireAuth } from '../../../../utils/auth'
|
|
import { createUserPasskey, getUserById, listUserPasskeys } from '../../../../utils/user-repository'
|
|
import { buildPasskeyLabel, consumeRegistrationChallenge, getWebAuthnConfig } from '../../../../utils/webauthn'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const auth = await requireAuth(event)
|
|
const body = await readBody<{
|
|
response?: RegistrationResponseJSON
|
|
}>(event)
|
|
|
|
if (!body.response) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Passkey registration response is required'
|
|
})
|
|
}
|
|
|
|
const expectedChallenge = await consumeRegistrationChallenge(auth.user.id)
|
|
|
|
if (!expectedChallenge) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Passkey registration challenge expired. Try again.'
|
|
})
|
|
}
|
|
|
|
const config = getWebAuthnConfig(event)
|
|
const verification = await verifyRegistrationResponse({
|
|
response: body.response,
|
|
expectedChallenge,
|
|
expectedOrigin: config.origin,
|
|
expectedRPID: config.rpID
|
|
})
|
|
|
|
if (!verification.verified || !verification.registrationInfo) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Passkey registration could not be verified'
|
|
})
|
|
}
|
|
|
|
try {
|
|
await createUserPasskey({
|
|
userId: auth.user.id,
|
|
credentialId: verification.registrationInfo.credential.id,
|
|
publicKey: verification.registrationInfo.credential.publicKey,
|
|
counter: verification.registrationInfo.credential.counter,
|
|
deviceType: verification.registrationInfo.credentialDeviceType,
|
|
backedUp: verification.registrationInfo.credentialBackedUp,
|
|
transports: body.response.response.transports || [],
|
|
label: buildPasskeyLabel()
|
|
})
|
|
} catch (error: any) {
|
|
if (error?.code === '23505') {
|
|
throw createError({
|
|
statusCode: 409,
|
|
statusMessage: 'This passkey is already registered'
|
|
})
|
|
}
|
|
|
|
throw error
|
|
}
|
|
|
|
const updatedUser = await getUserById(auth.user.id)
|
|
const passkeys = await listUserPasskeys(auth.user.id)
|
|
|
|
return {
|
|
ok: true,
|
|
user: updatedUser,
|
|
passkeys
|
|
}
|
|
})
|