diff --git a/pages/registrieren.vue b/pages/registrieren.vue index d651bcf..b4a1f66 100644 --- a/pages/registrieren.vue +++ b/pages/registrieren.vue @@ -630,6 +630,18 @@ const handleRegisterWithPasskey = async () => { }) // Neue API-Struktur: { optionsJSON: options } + // WICHTIG: Bei Cross-Device wird der QR-Code automatisch generiert + // Das Smartphone scannt den QR-Code und öffnet die Website + // Dann ruft das Smartphone startRegistration auf, um die Credential zu erstellen + // Die Credential-Response wird dann an den Desktop-Browser zurückgesendet + console.log('[DEBUG] About to call startRegistration - this will trigger QR code generation for Cross-Device') + console.log('[DEBUG] If using Cross-Device:') + console.log('[DEBUG] 1. QR-Code wird angezeigt') + console.log('[DEBUG] 2. Smartphone scannt QR-Code') + console.log('[DEBUG] 3. Smartphone öffnet:', window.location.origin) + console.log('[DEBUG] 4. Smartphone muss startRegistration aufrufen können') + console.log('[DEBUG] 5. Smartphone sendet Credential-Response an:', window.location.origin + '/api/auth/register-passkey') + credential = await mod.startRegistration({ optionsJSON: pre.options }) clearTimeout(timeoutWarning) @@ -640,7 +652,8 @@ const handleRegisterWithPasskey = async () => { credentialId: credential?.id, responseType: credential?.response?.constructor?.name, transports: credential?.transports, - durationSeconds: Math.round(webauthnDuration / 1000) + durationSeconds: Math.round(webauthnDuration / 1000), + note: 'Wenn Cross-Device verwendet wurde, sollte die Credential-Response vom Smartphone kommen' }) } catch (webauthnError) { const webauthnDuration = Date.now() - webauthnStart diff --git a/server/api/auth/register-passkey-options.options.js b/server/api/auth/register-passkey-options.options.js index 8f7615a..b3bd46a 100644 --- a/server/api/auth/register-passkey-options.options.js +++ b/server/api/auth/register-passkey-options.options.js @@ -4,10 +4,17 @@ export default defineEventHandler(async (event) => { const requestOrigin = getHeader(event, 'origin') const { origin: webauthnOrigin } = getWebAuthnConfig() - console.log('[DEBUG] OPTIONS preflight request received', { - origin: requestOrigin, + const userAgent = getHeader(event, 'user-agent') + const ip = getHeader(event, 'x-forwarded-for') || getHeader(event, 'x-real-ip') || 'unknown' + + console.log('[DEBUG] ===== OPTIONS preflight for /api/auth/register-passkey-options =====') + console.log('[DEBUG] OPTIONS Request Details:', { + origin: requestOrigin || 'none', webauthnOrigin, - timestamp: new Date().toISOString() + userAgent: userAgent?.substring(0, 100) || 'none', + ip, + timestamp: new Date().toISOString(), + note: 'OPTIONS Preflight für Passkey Registration Options' }) // CORS-Header für Cross-Device Authentication diff --git a/server/api/auth/register-passkey-options/[registrationId].get.js b/server/api/auth/register-passkey-options/[registrationId].get.js new file mode 100644 index 0000000..85efa4f --- /dev/null +++ b/server/api/auth/register-passkey-options/[registrationId].get.js @@ -0,0 +1,76 @@ +import { getPreRegistration } from '../../../utils/webauthn-challenges.js' +import { generateRegistrationOptions } from '@simplewebauthn/server' +import { getWebAuthnConfig } from '../../../utils/webauthn-config.js' + +export default defineEventHandler(async (event) => { + const registrationId = getRouterParam(event, 'registrationId') + const requestOrigin = getHeader(event, 'origin') + const userAgent = getHeader(event, 'user-agent') + + console.log('[DEBUG] ===== GET register-passkey-options/[registrationId] =====') + console.log('[DEBUG] Request Details:', { + registrationId, + origin: requestOrigin, + userAgent: userAgent?.substring(0, 100), + timestamp: new Date().toISOString(), + method: getMethod(event), + note: 'Dieser Endpoint wird vom Smartphone aufgerufen, um die Options für Cross-Device zu erhalten' + }) + + if (!registrationId) { + throw createError({ statusCode: 400, message: 'registrationId ist erforderlich' }) + } + + // Hole Pre-Registration-Daten + const pre = getPreRegistration(registrationId) + if (!pre) { + console.error('[DEBUG] Pre-registration not found', { registrationId }) + throw createError({ statusCode: 404, message: 'Registrierungs-Session nicht gefunden oder abgelaufen' }) + } + + const { challenge, userId, name, email } = pre + console.log('[DEBUG] Pre-registration found', { + userId, + email: email?.substring(0, 10) + '...', + hasChallenge: !!challenge + }) + + const { rpId, rpName, origin: webauthnOrigin } = getWebAuthnConfig() + + // Generiere Options neu (mit der gespeicherten Challenge) + 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: 300000, + challenge: challenge // Verwende die gespeicherte Challenge + }) + + console.log('[DEBUG] Options regenerated for Cross-Device', { + hasChallenge: !!options.challenge, + challengeMatches: options.challenge === challenge, + rpId: options.rp?.id + }) + + // CORS-Header für Cross-Device + 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, OPTIONS') + setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Origin, X-Requested-With') + } + + return { + success: true, + registrationId, + options + } +}) diff --git a/server/api/auth/register-passkey.options.js b/server/api/auth/register-passkey.options.js index b394186..6a1cfbd 100644 --- a/server/api/auth/register-passkey.options.js +++ b/server/api/auth/register-passkey.options.js @@ -4,10 +4,17 @@ export default defineEventHandler(async (event) => { const requestOrigin = getHeader(event, 'origin') const { origin: webauthnOrigin } = getWebAuthnConfig() - console.log('[DEBUG] OPTIONS preflight request received (register-passkey)', { - origin: requestOrigin, + const userAgent = getHeader(event, 'user-agent') + const ip = getHeader(event, 'x-forwarded-for') || getHeader(event, 'x-real-ip') || 'unknown' + + console.log('[DEBUG] ===== OPTIONS preflight for /api/auth/register-passkey =====') + console.log('[DEBUG] OPTIONS Request Details:', { + origin: requestOrigin || 'none', webauthnOrigin, - timestamp: new Date().toISOString() + userAgent: userAgent?.substring(0, 100) || 'none', + ip, + timestamp: new Date().toISOString(), + note: 'OPTIONS Preflight für Cross-Device Passkey - Wenn dieser Request vom Smartphone kommt, sollte der User-Agent Mobile/Android/iPhone enthalten' }) // CORS-Header für Cross-Device Authentication diff --git a/server/api/auth/register-passkey.post.js b/server/api/auth/register-passkey.post.js index 5212230..2a2b8fc 100644 --- a/server/api/auth/register-passkey.post.js +++ b/server/api/auth/register-passkey.post.js @@ -15,16 +15,27 @@ export default defineEventHandler(async (event) => { const userAgent = getHeader(event, 'user-agent') const { origin: webauthnOrigin } = getWebAuthnConfig() - console.log('[DEBUG] ===== register-passkey request received =====') + console.log('') + console.log('='.repeat(80)) + console.log('[DEBUG] 🔑 ===== PASSKEY REGISTRATION REQUEST RECEIVED =====') + console.log('='.repeat(80)) console.log('[DEBUG] Request Details:', { - origin: requestOrigin, + origin: requestOrigin || 'none', webauthnOrigin, - userAgent: userAgent?.substring(0, 150), + userAgent: userAgent?.substring(0, 150) || 'none', timestamp: new Date().toISOString(), method: getMethod(event), ip: getClientIp(event), - note: 'Dieser Request sollte vom Smartphone kommen, wenn der QR-Code gescannt wurde' + url: getRequestURL(event).href, + path: getRequestURL(event).pathname }) + console.log('[DEBUG] ⚠️ WICHTIG: Dieser Request sollte vom Smartphone kommen, wenn der QR-Code gescannt wurde') + console.log('[DEBUG] ⚠️ Wenn dieser Request NICHT im Log erscheint, bedeutet das:') + console.log('[DEBUG] 1. Das Smartphone konnte die Website nicht erreichen') + console.log('[DEBUG] 2. Oder startRegistration auf dem Smartphone ist fehlgeschlagen') + console.log('[DEBUG] 3. Oder die Credential-Response wurde nicht gesendet') + console.log('='.repeat(80)) + console.log('') console.log('[DEBUG] User-Agent Analysis:', { isMobile: /Mobile|Android|iPhone|iPad/i.test(userAgent || ''), isChrome: /Chrome/i.test(userAgent || ''), diff --git a/server/middleware/log-requests.js b/server/middleware/log-requests.js new file mode 100644 index 0000000..8f0d715 --- /dev/null +++ b/server/middleware/log-requests.js @@ -0,0 +1,41 @@ +/** + * Globales Request-Logging für Debugging + * Loggt alle Requests, besonders Passkey-relevante Endpoints + */ + +export default defineEventHandler((event) => { + const url = getRequestURL(event) + const method = getMethod(event) + const path = url.pathname + const origin = getHeader(event, 'origin') + const userAgent = getHeader(event, 'user-agent') + const ip = getHeader(event, 'x-forwarded-for') || getHeader(event, 'x-real-ip') || 'unknown' + + // Logge nur Passkey-relevante Endpoints (um Logs nicht zu überfluten) + const passkeyEndpoints = [ + '/api/auth/register-passkey', + '/api/auth/register-passkey-options', + '/api/auth/passkeys/registration-options', + '/api/auth/passkeys/register', + '/api/auth/passkeys/authentication-options', + '/api/auth/passkeys/login', + '/api/auth/passkeys/recovery' + ] + + const isPasskeyEndpoint = passkeyEndpoints.some(ep => path.startsWith(ep)) + + if (isPasskeyEndpoint) { + const timestamp = new Date().toISOString() + console.log(`[REQUEST] ${timestamp} ${method} ${path}`) + console.log(`[REQUEST] Origin: ${origin || 'none'}`) + console.log(`[REQUEST] IP: ${ip}`) + console.log(`[REQUEST] User-Agent: ${userAgent?.substring(0, 100) || 'none'}`) + + // Spezielle Logs für Cross-Device + if (path.includes('register-passkey')) { + console.log(`[REQUEST] 🔑 PASSKEY REQUEST - ${method} ${path}`) + console.log(`[REQUEST] ⚠️ Wenn dieser Request vom Smartphone kommt, sollte der User-Agent Mobile/Android/iPhone enthalten`) + console.log(`[REQUEST] ⚠️ Wenn dieser Request NICHT kommt, erreicht das Smartphone den Server nicht`) + } + } +}) diff --git a/server/utils/webauthn-challenges.js b/server/utils/webauthn-challenges.js index 9ec4b2d..4455b4f 100644 --- a/server/utils/webauthn-challenges.js +++ b/server/utils/webauthn-challenges.js @@ -59,4 +59,12 @@ export function consumePreRegistration(registrationId) { return v.payload || null } +export function getPreRegistration(registrationId) { + cleanup(preRegChallenges) + const key = String(registrationId) + const v = preRegChallenges.get(key) + if (!v) return null + return v.payload || null +} +