Files
harheimertc/server/api/auth/register-passkey-options.post.js
Torsten Schulz (local) 994aabfb85
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 48s
Enhance WebAuthn origin handling and debug logging for passkey registration
Refine the WebAuthn configuration to ensure that HTTPS origins do not include ports, improving compliance with standards. Add additional debug logging in the passkey registration process to verify the webauthnOrigin and provide guidance for configuration issues, aiding in troubleshooting and enhancing the clarity of the registration flow.
2026-01-07 22:01:28 +01:00

158 lines
5.2 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')
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' })
}
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
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 }
}
// 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 }
})