Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 51s
Add comprehensive debug statements in the registrieren.vue component to validate the options structure and ensure the challenge format is correct before initiating registration. Update the register-passkey API to log additional request details, including client IP and user-agent analysis, to improve troubleshooting and provide better insights during the registration process.
301 lines
11 KiB
JavaScript
301 lines
11 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'
|
|
import { getClientIp } from '../../utils/rate-limit.js'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const requestStart = Date.now()
|
|
const requestOrigin = getHeader(event, 'origin')
|
|
const userAgent = getHeader(event, 'user-agent')
|
|
const { origin: webauthnOrigin } = getWebAuthnConfig()
|
|
|
|
console.log('[DEBUG] ===== register-passkey request received =====')
|
|
console.log('[DEBUG] Request Details:', {
|
|
origin: requestOrigin,
|
|
webauthnOrigin,
|
|
userAgent: userAgent?.substring(0, 150),
|
|
timestamp: new Date().toISOString(),
|
|
method: getMethod(event),
|
|
ip: getClientIp(event),
|
|
note: 'Dieser Request sollte vom Smartphone kommen, wenn der QR-Code gescannt wurde'
|
|
})
|
|
console.log('[DEBUG] User-Agent Analysis:', {
|
|
isMobile: /Mobile|Android|iPhone|iPad/i.test(userAgent || ''),
|
|
isChrome: /Chrome/i.test(userAgent || ''),
|
|
isSafari: /Safari/i.test(userAgent || '') && !/Chrome/i.test(userAgent || ''),
|
|
isFirefox: /Firefox/i.test(userAgent || ''),
|
|
fullUserAgent: userAgent
|
|
})
|
|
|
|
// CORS-Header für Cross-Device Authentication
|
|
// 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', 'POST, OPTIONS')
|
|
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Origin, X-Requested-With')
|
|
console.log('[DEBUG] CORS headers set for POST', {
|
|
origin: allowedOrigin,
|
|
requestOrigin,
|
|
webauthnOrigin
|
|
})
|
|
}
|
|
|
|
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.'
|
|
}
|
|
})
|
|
|