diff --git a/pages/registrieren.vue b/pages/registrieren.vue index 489ceb1..e805018 100644 --- a/pages/registrieren.vue +++ b/pages/registrieren.vue @@ -324,7 +324,21 @@ const handleRegisterWithPasskey = async () => { } isLoading.value = true + + console.log('[DEBUG] Passkey-Registrierung gestartet', { + name: formData.value.name, + email: formData.value.email, + hasPassword: !!formData.value.password, + isPasskeySupported: isPasskeySupported.value, + userAgent: navigator.userAgent, + origin: window.location.origin, + isSecureContext: window.isSecureContext + }) + try { + console.log('[DEBUG] Requesting registration options from server...') + const requestStart = Date.now() + const pre = await $fetch('/api/auth/register-passkey-options', { method: 'POST', body: { @@ -333,35 +347,72 @@ const handleRegisterWithPasskey = async () => { phone: formData.value.phone } }) + + const requestDuration = Date.now() - requestStart + console.log(`[DEBUG] Options received (${requestDuration}ms)`, { + success: pre.success, + hasOptions: !!pre.options, + hasRegistrationId: !!pre.registrationId, + registrationId: pre.registrationId + }) if (!pre.success || !pre.options) { + console.error('[DEBUG] Invalid server response:', pre) throw new Error('Ungültige Antwort vom Server') } // Debug: Prüfe Options-Struktur - console.log('Received options:', { + console.log('[DEBUG] Options structure:', { hasChallenge: !!pre.options?.challenge, + challengeType: typeof pre.options?.challenge, + challengeLength: pre.options?.challenge?.length, hasRp: !!pre.options?.rp, + rpId: pre.options?.rp?.id, + rpName: pre.options?.rp?.name, hasUser: !!pre.options?.user, - timeout: pre.options?.timeout + userId: pre.options?.user?.id ? 'present' : 'missing', + userName: pre.options?.user?.name, + timeout: pre.options?.timeout, + pubKeyCredParams: pre.options?.pubKeyCredParams?.length, + authenticatorSelection: pre.options?.authenticatorSelection, + excludeCredentials: pre.options?.excludeCredentials?.length || 0 }) if (!pre.options || !pre.options.challenge) { - console.error('Options fehlen challenge:', pre.options) + console.error('[DEBUG] Options missing challenge:', JSON.stringify(pre.options, null, 2)) throw new Error('Ungültige WebAuthn-Options vom Server') } + console.log('[DEBUG] Calling startRegistration...') + const webauthnStart = Date.now() + const mod = await import('@simplewebauthn/browser') // startRegistration erwartet die Options direkt // @simplewebauthn/browser v13+ erwartet die Options direkt let credential try { credential = await mod.startRegistration(pre.options) + const webauthnDuration = Date.now() - webauthnStart + console.log(`[DEBUG] startRegistration completed (${webauthnDuration}ms)`, { + hasCredential: !!credential, + credentialId: credential?.id, + responseType: credential?.response?.constructor?.name, + transports: credential?.transports + }) } catch (webauthnError) { - console.error('WebAuthn startRegistration error:', webauthnError) + const webauthnDuration = Date.now() - webauthnStart + console.error(`[DEBUG] WebAuthn startRegistration failed (${webauthnDuration}ms):`, { + error: webauthnError, + message: webauthnError?.message, + name: webauthnError?.name, + stack: webauthnError?.stack + }) throw new Error('Passkey-Registrierung fehlgeschlagen: ' + (webauthnError?.message || 'Unbekannter Fehler')) } + console.log('[DEBUG] Sending credential to server...') + const verifyStart = Date.now() + const response = await $fetch('/api/auth/register-passkey', { method: 'POST', body: { @@ -370,13 +421,29 @@ const handleRegisterWithPasskey = async () => { password: setPasswordForPasskey.value ? formData.value.password : undefined } }) + + const verifyDuration = Date.now() - verifyStart + console.log(`[DEBUG] Server verification completed (${verifyDuration}ms)`, { + success: response.success, + message: response.message + }) if (response.success) { + console.log('[DEBUG] Registration successful!') successMessage.value = 'Registrierung erfolgreich! Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.' formData.value = { name: '', email: '', phone: '', password: '', confirmPassword: '' } setTimeout(() => navigateTo('/login'), 3000) + } else { + console.warn('[DEBUG] Registration response indicates failure:', response) } } catch (error) { + console.error('[DEBUG] Registration error:', { + error, + message: error?.message, + data: error?.data, + statusCode: error?.statusCode, + statusMessage: error?.statusMessage + }) errorMessage.value = error.data?.message || error?.message || 'Registrierung mit Passkey fehlgeschlagen.' } finally { isLoading.value = false diff --git a/server/api/auth/register-passkey-options.post.js b/server/api/auth/register-passkey-options.post.js index efe653f..c68cd89 100644 --- a/server/api/auth/register-passkey-options.post.js +++ b/server/api/auth/register-passkey-options.post.js @@ -10,15 +10,35 @@ function isValidEmail(email) { } export default defineEventHandler(async (event) => { + const requestStart = Date.now() + const requestOrigin = getHeader(event, 'origin') + const userAgent = getHeader(event, 'user-agent') + + console.log('[DEBUG] register-passkey-options request received', { + origin: requestOrigin, + userAgent: userAgent?.substring(0, 100), + method: getMethod(event), + timestamp: new Date().toISOString() + }) + 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' }) } @@ -27,11 +47,26 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 409, message: 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits' }) } - const { rpId, rpName } = getWebAuthnConfig() + const { rpId, rpName, origin: webauthnOrigin } = getWebAuthnConfig() + + console.log('[DEBUG] WebAuthn config', { + rpId, + rpName, + webauthnOrigin, + requestOrigin + }) 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, @@ -46,6 +81,21 @@ export default defineEventHandler(async (event) => { // Timeout erhöhen für Cross-Device (Standard: 60s, hier: 5 Minuten) timeout: 300000 }) + + 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, @@ -54,34 +104,29 @@ export default defineEventHandler(async (event) => { 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 - const requestOrigin = getHeader(event, 'origin') if (requestOrigin) { setHeader(event, 'Access-Control-Allow-Origin', requestOrigin) setHeader(event, 'Access-Control-Allow-Credentials', 'true') setHeader(event, 'Access-Control-Allow-Methods', 'POST, OPTIONS') setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization') + console.log('[DEBUG] CORS headers set', { origin: requestOrigin }) } if (getMethod(event) === 'OPTIONS') { + console.log('[DEBUG] OPTIONS request, returning early') return { success: true } } - // Debug: Log Options-Struktur - console.log('[WebAuthn Pre-Registration Options]', { - hasChallenge: !!options.challenge, - rpId: options.rp?.id, - userId: options.user?.id ? 'present' : 'missing', - timeout: options.timeout, - challengeType: typeof options.challenge - }) - // Stelle sicher, dass die Options korrekt serialisiert werden - // @simplewebauthn/server gibt ein Objekt zurück, das direkt JSON-serialisierbar ist - // Aber wir müssen sicherstellen, dass alle Properties vorhanden sind const serializedOptions = { ...options, challenge: options.challenge, @@ -91,6 +136,13 @@ export default defineEventHandler(async (event) => { authenticatorSelection: options.authenticatorSelection, timeout: options.timeout || 300000 } + + const totalDuration = Date.now() - requestStart + console.log(`[DEBUG] Returning options (total: ${totalDuration}ms)`, { + registrationId, + optionsKeys: Object.keys(serializedOptions), + serializedChallengeLength: serializedOptions.challenge?.length + }) return { success: true, registrationId, options: serializedOptions } }) diff --git a/server/api/auth/register-passkey.post.js b/server/api/auth/register-passkey.post.js index 94d5cb0..d879cf7 100644 --- a/server/api/auth/register-passkey.post.js +++ b/server/api/auth/register-passkey.post.js @@ -9,29 +9,61 @@ 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') + + console.log('[DEBUG] register-passkey request received', { + origin: requestOrigin, + timestamp: new Date().toISOString() + }) + 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() + console.log('[DEBUG] WebAuthn config for verification', { + origin, + rpId, + requireUV + }) + console.log('[DEBUG] Verifying registration response...') + const verifyStart = Date.now() + const verification = await verifyRegistrationResponse({ response, expectedChallenge: challenge, @@ -40,8 +72,22 @@ export default defineEventHandler(async (event) => { requireUserVerification: requireUV }) + 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' }) } @@ -98,6 +144,14 @@ export default defineEventHandler(async (event) => { 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 })