Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 50s
Update the CORS header variable name from 'origin' to 'requestOrigin' in both login and registration API endpoints for improved clarity and consistency. This change enhances the readability of the code while maintaining support for cross-device authentication.
122 lines
4.3 KiB
JavaScript
122 lines
4.3 KiB
JavaScript
import { verifyAuthenticationResponse } from '@simplewebauthn/server'
|
|
import { createSession, generateToken, migrateUserRoles, readUsers, writeUsers } from '../../../utils/auth.js'
|
|
import { getWebAuthnConfig } from '../../../utils/webauthn-config.js'
|
|
import { consumeAuthChallenge } from '../../../utils/webauthn-challenges.js'
|
|
import { fromBase64Url, parseClientDataJSON } from '../../../utils/webauthn-encoding.js'
|
|
import { getAuthCookieOptions } from '../../../utils/cookies.js'
|
|
import { writeAuditLog } from '../../../utils/audit-log.js'
|
|
import { getClientIp } from '../../../utils/rate-limit.js'
|
|
|
|
function findUserByCredentialId(users, credentialId) {
|
|
const cid = String(credentialId || '')
|
|
for (const u of users) {
|
|
const pks = Array.isArray(u.passkeys) ? u.passkeys : []
|
|
const match = pks.find(pk => pk && pk.credentialId === cid)
|
|
if (match) return { user: u, passkey: match }
|
|
}
|
|
return { user: null, passkey: null }
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
// CORS-Header für Cross-Device Authentication
|
|
const requestOrigin = getHeader(event, 'origin')
|
|
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')
|
|
}
|
|
|
|
if (getMethod(event) === 'OPTIONS') {
|
|
return { success: true }
|
|
}
|
|
|
|
const ip = getClientIp(event)
|
|
const body = await readBody(event)
|
|
const response = body?.credential
|
|
if (!response) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Credential fehlt' })
|
|
}
|
|
|
|
// Challenge aus clientDataJSON holen
|
|
const clientData = parseClientDataJSON(response.response?.clientDataJSON)
|
|
const challenge = clientData?.challenge
|
|
if (!challenge) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Ungültige Passkey-Antwort (Challenge fehlt)' })
|
|
}
|
|
|
|
if (!consumeAuthChallenge(challenge)) {
|
|
await writeAuditLog('auth.passkey.login.failed', { ip, reason: 'unknown_or_expired_challenge' })
|
|
throw createError({ statusCode: 400, statusMessage: 'Login-Session abgelaufen. Bitte erneut versuchen.' })
|
|
}
|
|
|
|
const users = await readUsers()
|
|
const { user, passkey } = findUserByCredentialId(users, response.id)
|
|
if (!user || !passkey) {
|
|
await writeAuditLog('auth.passkey.login.failed', { ip, reason: 'credential_not_found' })
|
|
throw createError({ statusCode: 401, statusMessage: 'Passkey unbekannt' })
|
|
}
|
|
|
|
if (user.active === false) {
|
|
await writeAuditLog('auth.passkey.login.failed', { ip, userId: user.id, reason: 'inactive' })
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage: 'Ihr Konto wurde noch nicht freigeschaltet. Bitte warten Sie auf die Bestätigung des Vorstands.'
|
|
})
|
|
}
|
|
|
|
const { origin, rpId, requireUV } = getWebAuthnConfig()
|
|
|
|
const authenticator = {
|
|
credentialID: fromBase64Url(passkey.credentialId),
|
|
credentialPublicKey: fromBase64Url(passkey.publicKey),
|
|
counter: Number(passkey.counter) || 0,
|
|
transports: passkey.transports || undefined
|
|
}
|
|
|
|
const verification = await verifyAuthenticationResponse({
|
|
response,
|
|
expectedChallenge: challenge,
|
|
expectedOrigin: origin,
|
|
expectedRPID: rpId,
|
|
authenticator,
|
|
requireUserVerification: requireUV
|
|
})
|
|
|
|
if (!verification.verified) {
|
|
await writeAuditLog('auth.passkey.login.failed', { ip, userId: user.id, reason: 'verification_failed' })
|
|
throw createError({ statusCode: 401, statusMessage: 'Passkey-Login fehlgeschlagen' })
|
|
}
|
|
|
|
// Counter/lastUsed aktualisieren
|
|
passkey.counter = verification.authenticationInfo?.newCounter ?? passkey.counter
|
|
passkey.lastUsedAt = new Date().toISOString()
|
|
await writeUsers(users)
|
|
|
|
const token = generateToken(user)
|
|
await createSession(user.id, token)
|
|
|
|
setCookie(event, 'auth_token', token, { ...getAuthCookieOptions() })
|
|
|
|
await writeAuditLog('auth.passkey.login.success', { ip, userId: user.id })
|
|
|
|
const migratedUser = migrateUserRoles({ ...user })
|
|
const roles = Array.isArray(migratedUser.roles)
|
|
? migratedUser.roles
|
|
: (migratedUser.role ? [migratedUser.role] : ['mitglied'])
|
|
|
|
return {
|
|
success: true,
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
roles
|
|
},
|
|
role: roles[0] || 'mitglied'
|
|
}
|
|
})
|
|
|
|
|