From c40780ef892ee72c486392b948f849d64e0d04f6 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 7 Jan 2026 21:36:41 +0100 Subject: [PATCH] Enhance passkey registration process with detailed debug logging and validation checks Add comprehensive debug logging throughout the passkey registration flow, including request handling, option generation, and verification steps. Implement validation for incoming requests and responses to ensure required fields are present, improving error handling and clarity. This update aims to facilitate troubleshooting and enhance the overall robustness of the registration process. --- pages/registrieren.vue | 75 +++++++++++++++++- .../api/auth/register-passkey-options.post.js | 78 +++++++++++++++---- server/api/auth/register-passkey.post.js | 54 +++++++++++++ 3 files changed, 190 insertions(+), 17 deletions(-) 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 })