Files
harheimertc/server/api/auth/register-passkey.post.js
Torsten Schulz (local) a16838ff47
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 50s
Enhance debug logging and validation in passkey registration process
Add detailed debug logging to track the verification parameters and errors during the passkey registration flow. Implement validation to ensure the expected origin does not include port 3100, improving error handling and providing clear guidance for configuration issues. This update aims to enhance troubleshooting and the overall robustness of the registration process.
2026-01-07 22:05:24 +01:00

270 lines
9.3 KiB
JavaScript

import { verifyRegistrationResponse } from '@simplewebauthn/server'
import crypto from 'crypto'
import nodemailer from 'nodemailer'
import { hashPassword, readUsers, writeUsers } from '../../utils/auth.js'
import { getWebAuthnConfig } from '../../utils/webauthn-config.js'
import { consumePreRegistration } from '../../utils/webauthn-challenges.js'
import { toBase64Url } from '../../utils/webauthn-encoding.js'
import { writeAuditLog } from '../../utils/audit-log.js'
import { assertPasswordNotPwned } from '../../utils/hibp.js'
export default defineEventHandler(async (event) => {
const requestStart = Date.now()
const requestOrigin = getHeader(event, 'origin')
console.log('[DEBUG] register-passkey request received', {
origin: requestOrigin,
timestamp: new Date().toISOString()
})
const body = await readBody(event)
const registrationId = String(body?.registrationId || '')
const response = body?.credential
const password = body?.password ? String(body.password) : ''
console.log('[DEBUG] Request body parsed', {
hasRegistrationId: !!registrationId,
registrationId: registrationId.substring(0, 10) + '...',
hasCredential: !!response,
credentialId: response?.id,
hasPassword: !!password
})
if (!registrationId || !response) {
console.error('[DEBUG] Validation failed: missing registrationId or credential')
throw createError({ statusCode: 400, statusMessage: 'Ungültige Anfrage' })
}
const pre = consumePreRegistration(registrationId)
if (!pre) {
console.error('[DEBUG] Pre-registration not found or expired', { registrationId })
throw createError({ statusCode: 400, statusMessage: 'Registrierungs-Session abgelaufen. Bitte erneut versuchen.' })
}
const { challenge, userId, name, email, phone } = pre
console.log('[DEBUG] Pre-registration found', {
userId,
email: email.substring(0, 10) + '...',
hasChallenge: !!challenge
})
const users = await readUsers()
if (users.some(u => String(u.email || '').toLowerCase() === String(email).toLowerCase())) {
console.error('[DEBUG] User already exists', { email })
throw createError({ statusCode: 409, message: 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits' })
}
const { origin, rpId, requireUV } = getWebAuthnConfig()
// Debug: Prüfe die tatsächliche Origin aus der Response
const clientData = response?.response?.clientDataJSON
let actualOrigin = null
if (clientData) {
try {
const decoded = Buffer.from(clientData, 'base64').toString('utf-8')
const parsed = JSON.parse(decoded)
actualOrigin = parsed.origin
console.log('[DEBUG] Parsed clientDataJSON', {
origin: parsed.origin,
type: parsed.type,
challenge: parsed.challenge ? 'present' : 'missing'
})
} catch (e) {
console.warn('[DEBUG] Could not parse clientDataJSON:', e)
}
}
console.log('[DEBUG] WebAuthn config for verification', {
expectedOrigin: origin,
expectedOriginType: typeof origin,
expectedOriginLength: origin?.length,
actualOriginFromResponse: actualOrigin,
rpId,
requireUV,
originMatch: origin === actualOrigin,
webauthnOriginEnv: process.env.WEBAUTHN_ORIGIN,
baseUrlEnv: process.env.NUXT_PUBLIC_BASE_URL
})
// WICHTIG: Sicherstellen, dass die Origin KEINEN Port hat
if (origin && origin.includes(':3100')) {
console.error('[DEBUG] ERROR: expectedOrigin contains port 3100! This will cause verification to fail.')
console.error('[DEBUG] Fix: Set WEBAUTHN_ORIGIN=https://harheimertc.tsschulz.de (without port) in .env')
throw createError({
statusCode: 500,
statusMessage: 'WebAuthn-Konfiguration fehlerhaft: Origin enthält Port 3100. Bitte WEBAUTHN_ORIGIN in .env korrigieren.'
})
}
console.log('[DEBUG] Verifying registration response...')
console.log('[DEBUG] Verification parameters', {
expectedOrigin: origin,
expectedRPID: rpId,
hasChallenge: !!challenge,
challengeLength: challenge?.length,
hasResponse: !!response,
responseId: response?.id
})
const verifyStart = Date.now()
let verification
try {
verification = await verifyRegistrationResponse({
response,
expectedChallenge: challenge,
expectedOrigin: origin,
expectedRPID: rpId,
requireUserVerification: requireUV
})
} catch (verifyError) {
const verifyDuration = Date.now() - verifyStart
console.error(`[DEBUG] Verification error (${verifyDuration}ms):`, {
error: verifyError,
message: verifyError?.message,
cause: verifyError?.cause?.message,
expectedOrigin: origin,
actualOriginFromResponse: actualOrigin,
stack: verifyError?.stack
})
throw verifyError
}
const verifyDuration = Date.now() - verifyStart
const { verified, registrationInfo } = verification
console.log(`[DEBUG] Verification completed (${verifyDuration}ms)`, {
verified,
hasRegistrationInfo: !!registrationInfo,
credentialId: registrationInfo?.credentialID ? 'present' : 'missing',
deviceType: registrationInfo?.credentialDeviceType,
backedUp: registrationInfo?.credentialBackedUp
})
if (!verified || !registrationInfo) {
console.error('[DEBUG] Verification failed', {
verified,
hasRegistrationInfo: !!registrationInfo
})
await writeAuditLog('auth.passkey.prereg.failed', { email })
throw createError({ statusCode: 400, statusMessage: 'Passkey-Registrierung fehlgeschlagen' })
}
const {
credentialID,
credentialPublicKey,
counter,
credentialDeviceType,
credentialBackedUp
} = registrationInfo
const credentialId = toBase64Url(credentialID)
const publicKey = toBase64Url(credentialPublicKey)
// Optional: Passwort als Fallback (z.B. Firefox/Linux) erlauben
let hashedPassword
if (password && password.trim().length > 0) {
if (password.length < 8) {
throw createError({ statusCode: 400, message: 'Das Passwort muss mindestens 8 Zeichen lang sein' })
}
await assertPasswordNotPwned(password)
hashedPassword = await hashPassword(password)
} else {
// Kein Passwort gesetzt: random Hash, damit bestehende Code-Pfade (verifyPassword) konsistent bleiben.
hashedPassword = await hashPassword(crypto.randomBytes(32).toString('hex'))
}
const newUser = {
id: String(userId),
email: String(email).toLowerCase(),
password: hashedPassword,
name,
phone: phone || '',
role: 'mitglied',
active: false,
created: new Date().toISOString(),
lastLogin: null,
passkeys: [
{
id: `${Date.now()}`,
credentialId,
publicKey,
counter: Number(counter) || 0,
transports: Array.isArray(response.transports) ? response.transports : undefined,
deviceType: credentialDeviceType,
backedUp: !!credentialBackedUp,
createdAt: new Date().toISOString(),
lastUsedAt: null,
name: 'Passkey'
}
]
}
users.push(newUser)
await writeUsers(users)
const totalDuration = Date.now() - requestStart
console.log(`[DEBUG] User created successfully (total: ${totalDuration}ms)`, {
userId: newUser.id,
email: newUser.email.substring(0, 10) + '...',
hasPasskey: newUser.passkeys?.length > 0,
hasPassword: !!newUser.password
})
await writeAuditLog('auth.passkey.prereg.success', { email, userId: newUser.id })
// Send notification emails (same behavior as password registration)
try {
const smtpUser = process.env.SMTP_USER
const smtpPass = process.env.SMTP_PASS
if (smtpUser && smtpPass) {
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: process.env.SMTP_PORT || 587,
secure: false,
auth: { user: smtpUser, pass: smtpPass }
})
await transporter.sendMail({
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
to: process.env.SMTP_ADMIN || 'j.dichmann@gmx.de',
subject: 'Neue Registrierung (Passkey) - Harheimer TC',
html: `
<h2>Neue Registrierung (Passkey)</h2>
<p>Ein neuer Benutzer hat sich registriert und wartet auf Freigabe:</p>
<ul>
<li><strong>Name:</strong> ${name}</li>
<li><strong>E-Mail:</strong> ${email}</li>
<li><strong>Telefon:</strong> ${phone || 'Nicht angegeben'}</li>
<li><strong>Login:</strong> Passkey${password ? ' + Passwort (Fallback)' : ' (ohne Passwort)'}</li>
</ul>
<p>Bitte prüfen Sie die Registrierung im CMS.</p>
`
})
await transporter.sendMail({
from: process.env.SMTP_FROM || 'noreply@harheimertc.de',
to: email,
subject: 'Registrierung erhalten - Harheimer TC',
html: `
<h2>Registrierung erhalten</h2>
<p>Hallo ${name},</p>
<p>vielen Dank für Ihre Registrierung beim Harheimer TC!</p>
<p>Ihre Anfrage wird vom Vorstand geprüft. Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.</p>
<br>
<p>Mit sportlichen Grüßen,<br>Ihr Harheimer TC</p>
`
})
}
} catch (emailError) {
console.error('E-Mail-Versand fehlgeschlagen:', emailError)
}
return {
success: true,
message: 'Registrierung erfolgreich. Sie erhalten eine E-Mail, sobald Ihr Zugang freigeschaltet wurde.'
}
})