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
This commit is contained in:
80
server/api/auth/passkey/login/verify.post.ts
Normal file
80
server/api/auth/passkey/login/verify.post.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user