Files
harheimertc/server/api/auth/register-passkey-options.post.js
Torsten Schulz (local) 34968742f0
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 55s
Add CORS testing documentation and HTML test page for Passkey Cross-Device Authentication
Introduce a comprehensive CORS testing guide in CORS_TEST_ANLEITUNG.md, detailing steps for testing OPTIONS and POST requests, along with expected responses. Additionally, add a new HTML test page (test-cors.html) to facilitate interactive testing of CORS headers and responses for the Passkey registration API. Update the server API to ensure proper CORS headers are set for Cross-Device Authentication, enhancing the overall testing and debugging process.
2026-01-08 11:14:22 +01:00

173 lines
5.7 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
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', {
origin: allowedOrigin,
requestOrigin,
webauthnOrigin,
method: getMethod(event)
})
}
// OPTIONS Preflight-Request für Cross-Device
if (getMethod(event) === 'OPTIONS') {
console.log('[DEBUG] OPTIONS preflight request, returning 204')
setResponseStatus(event, 204)
return null
}
// Stelle sicher, dass die Options korrekt serialisiert werden
const serializedOptions = {
...options,
challenge: options.challenge,
rp: options.rp,
user: options.user,
pubKeyCredParams: options.pubKeyCredParams,
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 }
})