import crypto from 'crypto' import { generateRegistrationOptions } from '@simplewebauthn/server' import { readUsers } from '../../utils/auth.js' import { getWebAuthnConfig } from '../../utils/webauthn-config.js' import { setPreRegistration } from '../../utils/webauthn-challenges.js' import { writeAuditLog } from '../../utils/audit-log.js' function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || '')) } export default defineEventHandler(async (event) => { const requestStart = Date.now() const requestOrigin = getHeader(event, 'origin') const userAgent = getHeader(event, 'user-agent') const nodeEnv = process.env.NODE_ENV || 'development' // Debug-Ausgaben immer ausgeben (nicht nur in dev) console.log('') console.log('='.repeat(80)) console.log('[DEBUG] 📋 ===== REGISTRATION OPTIONS REQUEST RECEIVED =====') console.log('='.repeat(80)) console.log('[DEBUG] Request Details:', { origin: requestOrigin || 'none', userAgent: userAgent?.substring(0, 150) || 'none', method: getMethod(event), timestamp: new Date().toISOString(), nodeEnv: nodeEnv, pid: process.pid, ip: getHeader(event, 'x-forwarded-for') || getHeader(event, 'x-real-ip') || 'unknown', note: 'Dieser Request kommt vom Desktop-Browser, wenn der QR-Code generiert wird' }) console.log('[DEBUG] User-Agent Analysis:', { isMobile: /Mobile|Android|iPhone|iPad/i.test(userAgent || ''), isDesktop: !/Mobile|Android|iPhone|iPad/i.test(userAgent || ''), browser: userAgent?.substring(0, 100) }) console.log('='.repeat(80)) console.log('') const body = await readBody(event) const name = String(body?.name || '').trim() const email = String(body?.email || '').trim().toLowerCase() const phone = String(body?.phone || '').trim() console.log('[DEBUG] Request body parsed', { hasName: !!name, hasEmail: !!email, hasPhone: !!phone, email: email.substring(0, 10) + '...' }) if (!name || !email) { console.error('[DEBUG] Validation failed: missing name or email') throw createError({ statusCode: 400, message: 'Name und E-Mail sind erforderlich' }) } if (!isValidEmail(email)) { console.error('[DEBUG] Validation failed: invalid email format') throw createError({ statusCode: 400, message: 'Ungültige E-Mail-Adresse' }) } const users = await readUsers() if (users.some(u => String(u.email || '').toLowerCase() === email)) { throw createError({ statusCode: 409, message: 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits' }) } const { rpId, rpName, origin: webauthnOrigin } = getWebAuthnConfig() console.log('[DEBUG] WebAuthn config', { rpId, rpName, webauthnOrigin, requestOrigin, webauthnOriginEnv: process.env.WEBAUTHN_ORIGIN, baseUrlEnv: process.env.NUXT_PUBLIC_BASE_URL }) // WICHTIG: Sicherstellen, dass die Origin KEINEN Port hat if (webauthnOrigin.includes(':3100')) { console.error('[DEBUG] ERROR: webauthnOrigin contains port 3100! This will cause verification to fail.') console.error('[DEBUG] Fix: Set WEBAUTHN_ORIGIN=https://harheimertc.tsschulz.de (without port) in .env') } const userId = crypto.randomUUID() const registrationId = crypto.randomBytes(16).toString('hex') console.log('[DEBUG] Generated IDs', { userId, registrationId }) console.log('[DEBUG] Generating registration options...') const optionsStart = Date.now() const options = await generateRegistrationOptions({ rpName, rpID: rpId, userID: new TextEncoder().encode(String(userId)), userName: email, userDisplayName: name, attestationType: 'none', authenticatorSelection: { residentKey: 'preferred', userVerification: 'preferred' }, // Timeout erhöhen für Cross-Device (Standard: 60s, hier: 5 Minuten) timeout: 300000 // HINWEIS: FIDO Cross-Device verwendet Tunnel-Server (z.B. cable.ua5v.com von Google) // Der Browser verwaltet den Tunnel automatisch - der Server muss nichts konfigurieren // Für Cross-Device funktioniert es so: // 1. Desktop-Browser generiert QR-Code mit öffentlichem Schlüssel (FIDO-URI) // 2. Desktop-Browser registriert sich beim Tunnel-Server (cable.ua5v.com) // 3. Smartphone scannt QR-Code // 4. Smartphone verbindet sich über Tunnel-Server mit Desktop-Browser // 5. Desktop-Browser leitet Credential-Response an den Server weiter // Voraussetzungen: // - Beide Geräte müssen Internetverbindung haben // - Tunnel-Server müssen erreichbar sein (cable.ua5v.com, cable.auth.com) // - Bluetooth kann für physische Nähe-Bestätigung verwendet werden (abhängig vom Browser/Gerät) }) const optionsDuration = Date.now() - optionsStart console.log(`[DEBUG] Registration options generated (${optionsDuration}ms)`, { hasChallenge: !!options.challenge, challengeLength: options.challenge?.length, rpId: options.rp?.id, rpName: options.rp?.name, userId: options.user?.id ? 'present' : 'missing', userName: options.user?.name, userDisplayName: options.user?.displayName, timeout: options.timeout, pubKeyCredParamsCount: options.pubKeyCredParams?.length, authenticatorSelection: options.authenticatorSelection, excludeCredentialsCount: options.excludeCredentials?.length || 0 }) setPreRegistration(registrationId, { challenge: options.challenge, userId, name, email, phone }) console.log('[DEBUG] Pre-registration stored', { registrationId, challengeStored: !!options.challenge }) await writeAuditLog('auth.passkey.prereg.options', { email }) // CORS-Header für Cross-Device Authentication // WICHTIG: Für Cross-Device muss CORS korrekt konfiguriert sein // 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', 'GET, POST, OPTIONS') setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Origin, X-Requested-With') setHeader(event, 'Access-Control-Max-Age', '86400') // 24 Stunden Cache für Preflight console.log('[DEBUG] CORS headers set for POST', { origin: allowedOrigin, requestOrigin, webauthnOrigin }) } // Options direkt zurückgeben (wie in passkeys/registration-options.post.js) // @simplewebauthn/server gibt bereits korrekt formatierte Options zurück const totalDuration = Date.now() - requestStart // Debug: Prüfe die vollständige Options-Struktur console.log(`[DEBUG] Returning options (total: ${totalDuration}ms)`, { registrationId, optionsKeys: Object.keys(options), challengeLength: options.challenge?.length, challengeType: typeof options.challenge, rpId: options.rp?.id, rpName: options.rp?.name, userIdType: typeof options.user?.id, userName: options.user?.name, userDisplayName: options.user?.displayName, timeout: options.timeout, timeoutType: typeof options.timeout, pubKeyCredParamsCount: options.pubKeyCredParams?.length, authenticatorSelection: options.authenticatorSelection, hasExtensions: !!options.extensions, hasHints: !!options.hints, excludeCredentialsCount: options.excludeCredentials?.length || 0 }) // WICHTIG: Prüfe, ob die Options für Cross-Device korrekt sind // Für Cross-Device muss die Challenge ein String sein (Base64URL) if (typeof options.challenge !== 'string') { console.error('[DEBUG] ERROR: Challenge is not a string!', typeof options.challenge, options.challenge) } // Prüfe, ob user.id ein Uint8Array ist (wird zu Base64URL konvertiert) if (options.user?.id instanceof Uint8Array) { console.log('[DEBUG] user.id is Uint8Array (will be converted to Base64URL by browser)') } else { console.log('[DEBUG] user.id type:', typeof options.user?.id, 'value:', options.user?.id?.substring?.(0, 20)) } // WICHTIG: Options direkt zurückgeben, keine manuelle Serialisierung // Die Options von @simplewebauthn/server sind bereits korrekt formatiert // Nuxt/Nitro serialisiert automatisch zu JSON return { success: true, registrationId, options } })