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.
107 lines
3.4 KiB
JavaScript
107 lines
3.4 KiB
JavaScript
import { verifyRegistrationResponse } from '@simplewebauthn/server'
|
|
import { getUserFromToken, readUsers, writeUsers } from '../../../utils/auth.js'
|
|
import { getWebAuthnConfig } from '../../../utils/webauthn-config.js'
|
|
import { clearRegistrationChallenge, getRegistrationChallenge } from '../../../utils/webauthn-challenges.js'
|
|
import { toBase64Url } from '../../../utils/webauthn-encoding.js'
|
|
import { writeAuditLog } from '../../../utils/audit-log.js'
|
|
|
|
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 token = getCookie(event, 'auth_token')
|
|
const user = token ? await getUserFromToken(token) : null
|
|
|
|
if (!user) {
|
|
throw createError({ statusCode: 401, statusMessage: 'Nicht authentifiziert' })
|
|
}
|
|
|
|
const body = await readBody(event)
|
|
const response = body?.credential
|
|
if (!response) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Credential fehlt' })
|
|
}
|
|
|
|
const expectedChallenge = getRegistrationChallenge(user.id)
|
|
if (!expectedChallenge) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Registrierungs-Session abgelaufen. Bitte erneut versuchen.' })
|
|
}
|
|
|
|
const { origin, rpId, requireUV } = getWebAuthnConfig()
|
|
|
|
let verification
|
|
try {
|
|
verification = await verifyRegistrationResponse({
|
|
response,
|
|
expectedChallenge,
|
|
expectedOrigin: origin,
|
|
expectedRPID: rpId,
|
|
requireUserVerification: requireUV
|
|
})
|
|
} finally {
|
|
clearRegistrationChallenge(user.id)
|
|
}
|
|
|
|
const { verified, registrationInfo } = verification
|
|
if (!verified || !registrationInfo) {
|
|
await writeAuditLog('auth.passkey.registration.failed', { userId: user.id })
|
|
throw createError({ statusCode: 400, statusMessage: 'Passkey-Registrierung fehlgeschlagen' })
|
|
}
|
|
|
|
const {
|
|
credentialID,
|
|
credentialPublicKey,
|
|
counter,
|
|
credentialDeviceType,
|
|
credentialBackedUp
|
|
} = registrationInfo
|
|
|
|
const credentialId = toBase64Url(credentialID)
|
|
const publicKey = toBase64Url(credentialPublicKey)
|
|
|
|
const users = await readUsers()
|
|
const idx = users.findIndex(u => u.id === user.id)
|
|
if (idx === -1) {
|
|
throw createError({ statusCode: 404, statusMessage: 'Benutzer nicht gefunden' })
|
|
}
|
|
|
|
const u = users[idx]
|
|
if (!Array.isArray(u.passkeys)) u.passkeys = []
|
|
|
|
// Duplikate verhindern
|
|
if (u.passkeys.some(pk => pk.credentialId === credentialId)) {
|
|
return { success: true, message: 'Passkey ist bereits registriert.' }
|
|
}
|
|
|
|
u.passkeys.push({
|
|
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: body?.name ? String(body.name).slice(0, 80) : 'Passkey'
|
|
})
|
|
|
|
users[idx] = u
|
|
await writeUsers(users)
|
|
|
|
await writeAuditLog('auth.passkey.registered', { userId: user.id })
|
|
return { success: true, message: 'Passkey hinzugefügt.' }
|
|
})
|
|
|
|
|