Files
harheimertc/server/api/auth/passkeys/recovery/complete.post.js
Torsten Schulz (local) 58fd7fa5c6
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 5m7s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
feat(auth): implement Android refresh token handling and session management
- Added support for generating Android access tokens and managing refresh sessions in the auth endpoints.
- Implemented new tests for login, logout, and refresh functionalities specific to Android clients.
- Enhanced password reset logging with normalization and masking of email addresses.
- Created a new diagnostics endpoint for password reset attempts, including filtering and summarizing logs.
- Introduced a new utility for managing password reset logs with retention policies.
- Added tests for password reset log utilities to ensure proper functionality and privacy compliance.
- Updated WebAuthn configuration tests to validate origin handling for production and allowed origins.
2026-05-27 19:34:53 +02:00

126 lines
4.3 KiB
JavaScript

import { verifyRegistrationResponse } from '@simplewebauthn/server'
import { readUsers, writeUsers } from '../../../../utils/auth.js'
import { consumePreRegistration } from '../../../../utils/webauthn-challenges.js'
import { getWebAuthnConfig } from '../../../../utils/webauthn-config.js'
import { toBase64Url } from '../../../../utils/webauthn-encoding.js'
import { assertRateLimit, getClientIp } from '../../../../utils/rate-limit.js'
import { writeAuditLog } from '../../../../utils/audit-log.js'
function findUserAndToken(users, userId, tokenHash) {
const now = Date.now()
const idx = users.findIndex(u => String(u.id) === String(userId))
if (idx === -1) return { idx: -1, tokenEntry: null }
const user = users[idx]
const list = Array.isArray(user.passkeyRecoveryTokens) ? user.passkeyRecoveryTokens : []
const tokenEntry = list.find(t =>
t &&
t.tokenHash === tokenHash &&
!t.usedAt &&
t.expiresAt &&
new Date(t.expiresAt).getTime() > now
)
return { idx, tokenEntry }
}
export default defineEventHandler(async (event) => {
const ip = getClientIp(event)
const body = await readBody(event)
const recoveryId = String(body?.recoveryId || '')
const response = body?.credential
const passkeyName = body?.name ? String(body.name).slice(0, 80) : 'Passkey'
if (!recoveryId || !response) {
throw createError({ statusCode: 400, statusMessage: 'Ungültige Anfrage' })
}
assertRateLimit(event, {
name: 'auth:passkey-recovery:complete:ip',
keyParts: [ip],
windowMs: 10 * 60 * 1000,
maxAttempts: 30,
lockoutMs: 10 * 60 * 1000
})
const pre = consumePreRegistration(recoveryId)
if (!pre) {
throw createError({ statusCode: 400, statusMessage: 'Recovery-Session abgelaufen. Bitte erneut versuchen.' })
}
const { challenge, tokenHash, userId } = pre
assertRateLimit(event, {
name: 'auth:passkey-recovery:complete:token',
keyParts: [tokenHash],
windowMs: 10 * 60 * 1000,
maxAttempts: 10,
lockoutMs: 30 * 60 * 1000
})
const users = await readUsers()
const { idx, tokenEntry } = findUserAndToken(users, userId, tokenHash)
if (idx === -1 || !tokenEntry) {
await writeAuditLog('auth.passkey.recovery.complete.failed', { ip, reason: 'invalid_token', userId })
throw createError({ statusCode: 400, statusMessage: 'Ungültiger oder abgelaufener Token' })
}
const { origins, rpId, requireUV } = getWebAuthnConfig()
let verification
try {
verification = await verifyRegistrationResponse({
response,
expectedChallenge: challenge,
expectedOrigin: origins,
expectedRPID: rpId,
requireUserVerification: requireUV
})
} catch {
await writeAuditLog('auth.passkey.recovery.complete.failed', { ip, reason: 'verification_error', userId })
throw createError({ statusCode: 400, statusMessage: 'Passkey-Registrierung fehlgeschlagen' })
}
const { verified, registrationInfo } = verification
if (!verified || !registrationInfo) {
await writeAuditLog('auth.passkey.recovery.complete.failed', { ip, reason: 'verification_failed', userId })
throw createError({ statusCode: 400, statusMessage: 'Passkey-Registrierung fehlgeschlagen' })
}
const {
credentialID,
credentialPublicKey,
counter,
credentialDeviceType,
credentialBackedUp
} = registrationInfo
const credentialId = toBase64Url(credentialID)
const publicKey = toBase64Url(credentialPublicKey)
const user = users[idx]
if (!Array.isArray(user.passkeys)) user.passkeys = []
// Duplikate verhindern
if (!user.passkeys.some(pk => pk.credentialId === credentialId)) {
user.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: passkeyName
})
}
// Token als benutzt markieren (one-time)
tokenEntry.usedAt = new Date().toISOString()
users[idx] = user
await writeUsers(users)
await writeAuditLog('auth.passkey.recovery.complete.success', { ip, userId: user.id })
return { success: true, message: 'Passkey hinzugefügt. Sie können sich jetzt mit dem neuen Passkey anmelden.' }
})