Files
harheimertc/server/api/auth/register-passkey-options.post.js
Torsten Schulz (local) 04f38cda69
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 54s
Enhance debug logging and validation in Passkey Registration API
Add additional debug statements to the register-passkey-options API to validate the options structure, including checks for challenge type and user ID format. Improve logging to capture detailed information about the options being returned, aiding in troubleshooting and ensuring compliance with Cross-Device requirements.
2026-01-08 15:57:19 +01:00

188 lines
6.8 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('[DEBUG] register-passkey-options request received', {
origin: requestOrigin,
userAgent: userAgent?.substring(0, 100),
method: getMethod(event),
timestamp: new Date().toISOString(),
nodeEnv: nodeEnv,
pid: process.pid
})
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
})
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 }
})