import { verifyAuthenticationResponse } from '@simplewebauthn/server' import { createSession, generateToken, migrateUserRoles, readUsers, writeUsers } from '../../../utils/auth.js' import { getWebAuthnConfig } from '../../../utils/webauthn-config.js' import { consumeAuthChallenge } from '../../../utils/webauthn-challenges.js' import { fromBase64Url, parseClientDataJSON } from '../../../utils/webauthn-encoding.js' import { getAuthCookieOptions } from '../../../utils/cookies.js' import { writeAuditLog } from '../../../utils/audit-log.js' import { getClientIp } from '../../../utils/rate-limit.js' function findUserByCredentialId(users, credentialId) { const cid = String(credentialId || '') for (const u of users) { const pks = Array.isArray(u.passkeys) ? u.passkeys : [] const match = pks.find(pk => pk && pk.credentialId === cid) if (match) return { user: u, passkey: match } } return { user: null, passkey: null } } export default defineEventHandler(async (event) => { const ip = getClientIp(event) const body = await readBody(event) const response = body?.credential if (!response) { throw createError({ statusCode: 400, statusMessage: 'Credential fehlt' }) } // Challenge aus clientDataJSON holen const clientData = parseClientDataJSON(response.response?.clientDataJSON) const challenge = clientData?.challenge if (!challenge) { throw createError({ statusCode: 400, statusMessage: 'Ungültige Passkey-Antwort (Challenge fehlt)' }) } if (!consumeAuthChallenge(challenge)) { await writeAuditLog('auth.passkey.login.failed', { ip, reason: 'unknown_or_expired_challenge' }) throw createError({ statusCode: 400, statusMessage: 'Login-Session abgelaufen. Bitte erneut versuchen.' }) } const users = await readUsers() const { user, passkey } = findUserByCredentialId(users, response.id) if (!user || !passkey) { await writeAuditLog('auth.passkey.login.failed', { ip, reason: 'credential_not_found' }) throw createError({ statusCode: 401, statusMessage: 'Passkey unbekannt' }) } if (user.active === false) { await writeAuditLog('auth.passkey.login.failed', { ip, userId: user.id, reason: 'inactive' }) throw createError({ statusCode: 403, statusMessage: 'Ihr Konto wurde noch nicht freigeschaltet. Bitte warten Sie auf die Bestätigung des Vorstands.' }) } const { origin, rpId, requireUV } = getWebAuthnConfig() const authenticator = { credentialID: fromBase64Url(passkey.credentialId), credentialPublicKey: fromBase64Url(passkey.publicKey), counter: Number(passkey.counter) || 0, transports: passkey.transports || undefined } const verification = await verifyAuthenticationResponse({ response, expectedChallenge: challenge, expectedOrigin: origin, expectedRPID: rpId, authenticator, requireUserVerification: requireUV }) if (!verification.verified) { await writeAuditLog('auth.passkey.login.failed', { ip, userId: user.id, reason: 'verification_failed' }) throw createError({ statusCode: 401, statusMessage: 'Passkey-Login fehlgeschlagen' }) } // Counter/lastUsed aktualisieren passkey.counter = verification.authenticationInfo?.newCounter ?? passkey.counter passkey.lastUsedAt = new Date().toISOString() await writeUsers(users) const token = generateToken(user) await createSession(user.id, token) setCookie(event, 'auth_token', token, { ...getAuthCookieOptions() }) await writeAuditLog('auth.passkey.login.success', { ip, userId: user.id }) const migratedUser = migrateUserRoles({ ...user }) const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied']) return { success: true, token, user: { id: user.id, email: user.email, name: user.name, roles }, role: roles[0] || 'mitglied' } })