import { verifyRegistrationResponse } from '@simplewebauthn/server' import crypto from 'crypto' import nodemailer from 'nodemailer' import { hashPassword, readUsers, writeUsers } from '../../utils/auth.js' import { getWebAuthnConfig } from '../../utils/webauthn-config.js' import { consumePreRegistration } from '../../utils/webauthn-challenges.js' import { toBase64Url } from '../../utils/webauthn-encoding.js' import { writeAuditLog } from '../../utils/audit-log.js' import { assertPasswordNotPwned } from '../../utils/hibp.js' export default defineEventHandler(async (event) => { const requestStart = Date.now() const requestOrigin = getHeader(event, 'origin') const userAgent = getHeader(event, 'user-agent') const { origin: webauthnOrigin } = getWebAuthnConfig() console.log('[DEBUG] register-passkey request received', { origin: requestOrigin, webauthnOrigin, userAgent: userAgent?.substring(0, 100), timestamp: new Date().toISOString(), method: getMethod(event), note: 'Dieser Request sollte vom Smartphone kommen, wenn der QR-Code gescannt wurde' }) // CORS-Header für Cross-Device Authentication // OPTIONS-Requests werden von .options.js behandelt const allowedOrigin = requestOrigin || webauthnOrigin if (allowedOrigin) { setHeader(event, 'Access-Control-Allow-Origin', allowedOrigin) setHeader(event, 'Access-Control-Allow-Credentials', 'true') setHeader(event, 'Access-Control-Allow-Methods', 'POST, OPTIONS') setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Origin, X-Requested-With') console.log('[DEBUG] CORS headers set for POST', { origin: allowedOrigin, requestOrigin, webauthnOrigin }) } const body = await readBody(event) const registrationId = String(body?.registrationId || '') const response = body?.credential const password = body?.password ? String(body.password) : '' console.log('[DEBUG] Request body parsed', { hasRegistrationId: !!registrationId, registrationId: registrationId.substring(0, 10) + '...', hasCredential: !!response, credentialId: response?.id, hasPassword: !!password }) if (!registrationId || !response) { console.error('[DEBUG] Validation failed: missing registrationId or credential') throw createError({ statusCode: 400, statusMessage: 'Ungültige Anfrage' }) } const pre = consumePreRegistration(registrationId) if (!pre) { console.error('[DEBUG] Pre-registration not found or expired', { registrationId }) throw createError({ statusCode: 400, statusMessage: 'Registrierungs-Session abgelaufen. Bitte erneut versuchen.' }) } const { challenge, userId, name, email, phone } = pre console.log('[DEBUG] Pre-registration found', { userId, email: email.substring(0, 10) + '...', hasChallenge: !!challenge }) const users = await readUsers() if (users.some(u => String(u.email || '').toLowerCase() === String(email).toLowerCase())) { console.error('[DEBUG] User already exists', { email }) throw createError({ statusCode: 409, message: 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits' }) } const { origin, rpId, requireUV } = getWebAuthnConfig() // Debug: Prüfe die tatsächliche Origin aus der Response const clientData = response?.response?.clientDataJSON let actualOrigin = null if (clientData) { try { const decoded = Buffer.from(clientData, 'base64').toString('utf-8') const parsed = JSON.parse(decoded) actualOrigin = parsed.origin console.log('[DEBUG] Parsed clientDataJSON', { origin: parsed.origin, type: parsed.type, challenge: parsed.challenge ? 'present' : 'missing' }) } catch (e) { console.warn('[DEBUG] Could not parse clientDataJSON:', e) } } console.log('[DEBUG] WebAuthn config for verification', { expectedOrigin: origin, expectedOriginType: typeof origin, expectedOriginLength: origin?.length, actualOriginFromResponse: actualOrigin, rpId, requireUV, originMatch: origin === actualOrigin, webauthnOriginEnv: process.env.WEBAUTHN_ORIGIN, baseUrlEnv: process.env.NUXT_PUBLIC_BASE_URL }) // WICHTIG: Sicherstellen, dass die Origin KEINEN Port hat if (origin && origin.includes(':3100')) { console.error('[DEBUG] ERROR: expectedOrigin contains port 3100! This will cause verification to fail.') console.error('[DEBUG] Fix: Set WEBAUTHN_ORIGIN=https://harheimertc.tsschulz.de (without port) in .env') throw createError({ statusCode: 500, statusMessage: 'WebAuthn-Konfiguration fehlerhaft: Origin enthält Port 3100. Bitte WEBAUTHN_ORIGIN in .env korrigieren.' }) } console.log('[DEBUG] Verifying registration response...') console.log('[DEBUG] Verification parameters', { expectedOrigin: origin, expectedRPID: rpId, hasChallenge: !!challenge, challengeLength: challenge?.length, hasResponse: !!response, responseId: response?.id }) const verifyStart = Date.now() let verification try { verification = await verifyRegistrationResponse({ response, expectedChallenge: challenge, expectedOrigin: origin, expectedRPID: rpId, requireUserVerification: requireUV }) } catch (verifyError) { const verifyDuration = Date.now() - verifyStart console.error(`[DEBUG] Verification error (${verifyDuration}ms):`, { error: verifyError, message: verifyError?.message, cause: verifyError?.cause?.message, expectedOrigin: origin, actualOriginFromResponse: actualOrigin, stack: verifyError?.stack }) throw verifyError } const verifyDuration = Date.now() - verifyStart const { verified, registrationInfo } = verification console.log(`[DEBUG] Verification completed (${verifyDuration}ms)`, { verified, hasRegistrationInfo: !!registrationInfo, credentialId: registrationInfo?.credentialID ? 'present' : 'missing', deviceType: registrationInfo?.credentialDeviceType, backedUp: registrationInfo?.credentialBackedUp }) if (!verified || !registrationInfo) { console.error('[DEBUG] Verification failed', { verified, hasRegistrationInfo: !!registrationInfo }) await writeAuditLog('auth.passkey.prereg.failed', { email }) throw createError({ statusCode: 400, statusMessage: 'Passkey-Registrierung fehlgeschlagen' }) } const { credentialID, credentialPublicKey, counter, credentialDeviceType, credentialBackedUp } = registrationInfo const credentialId = toBase64Url(credentialID) const publicKey = toBase64Url(credentialPublicKey) // Optional: Passwort als Fallback (z.B. Firefox/Linux) erlauben let hashedPassword if (password && password.trim().length > 0) { if (password.length < 8) { throw createError({ statusCode: 400, message: 'Das Passwort muss mindestens 8 Zeichen lang sein' }) } await assertPasswordNotPwned(password) hashedPassword = await hashPassword(password) } else { // Kein Passwort gesetzt: random Hash, damit bestehende Code-Pfade (verifyPassword) konsistent bleiben. hashedPassword = await hashPassword(crypto.randomBytes(32).toString('hex')) } const newUser = { id: String(userId), email: String(email).toLowerCase(), password: hashedPassword, name, phone: phone || '', role: 'mitglied', active: false, created: new Date().toISOString(), lastLogin: null, passkeys: [ { id: `${Date.now()}`, credentialId, publicKey, counter: Number(counter) || 0, transports: Array.isArray(response.transports) ? response.transports : undefined, deviceType: credentialDeviceType, backedUp: !!credentialBackedUp, createdAt: new Date().toISOString(), lastUsedAt: null, name: 'Passkey' } ] } users.push(newUser) await writeUsers(users) const totalDuration = Date.now() - requestStart console.log(`[DEBUG] User created successfully (total: ${totalDuration}ms)`, { userId: newUser.id, email: newUser.email.substring(0, 10) + '...', hasPasskey: newUser.passkeys?.length > 0, hasPassword: !!newUser.password }) await writeAuditLog('auth.passkey.prereg.success', { email, userId: newUser.id }) // Send notification emails (same behavior as password registration) try { const smtpUser = process.env.SMTP_USER const smtpPass = process.env.SMTP_PASS if (smtpUser && smtpPass) { const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'smtp.gmail.com', port: process.env.SMTP_PORT || 587, secure: false, auth: { user: smtpUser, pass: smtpPass } }) await transporter.sendMail({ from: process.env.SMTP_FROM || 'noreply@harheimertc.de', to: process.env.SMTP_ADMIN || 'j.dichmann@gmx.de', subject: 'Neue Registrierung (Passkey) - Harheimer TC', html: `
Ein neuer Benutzer hat sich registriert und wartet auf Freigabe:
Bitte prüfen Sie die Registrierung im CMS.
` }) await transporter.sendMail({ from: process.env.SMTP_FROM || 'noreply@harheimertc.de', to: email, subject: 'Registrierung erhalten - Harheimer TC', html: `Hallo ${name},
vielen Dank für Ihre Registrierung beim Harheimer TC!
Ihre Anfrage wird vom Vorstand geprüft. Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.
Mit sportlichen Grüßen,
Ihr Harheimer TC