Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 43s
Update the registrieren.vue component to enhance debug logging for local authenticator usage, providing clearer messages about the expected behavior during registration. Modify the register-passkey-options API to specify the use of local authenticators, ensuring better clarity on the authenticator selection process. This update aims to improve user understanding and troubleshooting during Passkey registration without the need for Cross-Device functionality.
226 lines
9.1 KiB
JavaScript
226 lines
9.1 KiB
JavaScript
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',
|
|
requireResidentKey: false,
|
|
// WICHTIG: authenticatorAttachment bestimmt, welche Authenticatoren verwendet werden können
|
|
// - 'platform': Nur lokale Authenticatoren (Windows Hello, TouchID, Browser-eigene Passkey-Speicherung)
|
|
// - 'cross-platform': Nur externe Authenticatoren (USB-Schlüssel, Smartphone via Cross-Device)
|
|
// - NICHT setzen: Browser entscheidet automatisch (kann lokale ODER Cross-Device verwenden)
|
|
//
|
|
// Für Browser-eigene Passkey-Speicherung: 'platform' verwenden
|
|
// Dies ermöglicht:
|
|
// - Browser-eigene Passkey-Speicherung (Chrome Password Manager, Firefox Password Manager, etc.)
|
|
// - Windows Hello (biometrisch oder PIN)
|
|
// - TouchID auf Mac
|
|
// - KEIN Cross-Device (kein Smartphone nötig)
|
|
authenticatorAttachment: 'platform' // Lokale Authenticatoren (Browser-eigene Passkey-Speicherung)
|
|
},
|
|
// 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 }
|
|
})
|
|
|