Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 45s
Update the registrieren.vue component to include detailed debug statements for the Cross-Device authentication flow, specifically during QR-Code generation. Improve logging in the register-passkey-options and register-passkey APIs to capture request details such as user agent and IP address, aiding in troubleshooting. Additionally, introduce a new function to retrieve pre-registration data, enhancing the overall registration process and compliance with Cross-Device requirements.
312 lines
11 KiB
JavaScript
312 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('')
|
|
console.log('='.repeat(80))
|
|
console.log('[DEBUG] 🔑 ===== PASSKEY REGISTRATION REQUEST RECEIVED =====')
|
|
console.log('='.repeat(80))
|
|
console.log('[DEBUG] Request Details:', {
|
|
origin: requestOrigin || 'none',
|
|
webauthnOrigin,
|
|
userAgent: userAgent?.substring(0, 150) || 'none',
|
|
timestamp: new Date().toISOString(),
|
|
method: getMethod(event),
|
|
ip: getClientIp(event),
|
|
url: getRequestURL(event).href,
|
|
path: getRequestURL(event).pathname
|
|
})
|
|
console.log('[DEBUG] ⚠️ WICHTIG: Dieser Request sollte vom Smartphone kommen, wenn der QR-Code gescannt wurde')
|
|
console.log('[DEBUG] ⚠️ Wenn dieser Request NICHT im Log erscheint, bedeutet das:')
|
|
console.log('[DEBUG] 1. Das Smartphone konnte die Website nicht erreichen')
|
|
console.log('[DEBUG] 2. Oder startRegistration auf dem Smartphone ist fehlgeschlagen')
|
|
console.log('[DEBUG] 3. Oder die Credential-Response wurde nicht gesendet')
|
|
console.log('='.repeat(80))
|
|
console.log('')
|
|
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.'
|
|
}
|
|
})
|
|
|