From 7b8efcff38fdc2f525d9f761d2bbe0b7e288d7ba Mon Sep 17 00:00:00 2001 From: Martin Porwoll Date: Sun, 7 Dec 2025 21:31:11 +0000 Subject: [PATCH] fix: complete auth event audit logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses remaining gaps from the audit review: 1. Register afterForgotPassword hook in Users collection - Password reset requests are now properly logged - Fixed hook signature (uses context instead of req) 2. Create custom /api/auth/login endpoint - Wraps native Payload login - Logs failed login attempts via auditLoginFailed - Returns proper error responses without exposing details 3. Export auditLoginFailed helper function - Can be used by other custom auth handlers - Calls logLoginFailed from audit-service Now all critical auth events are tracked: - Successful logins (afterLogin hook) - Failed logins (custom /api/auth/login endpoint) - Logouts (afterLogout hook) - Password reset requests (afterForgotPassword hook) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/app/(payload)/api/auth/login/route.ts | 134 ++++++++++++++++++++++ src/collections/Users.ts | 7 +- src/hooks/auditAuthEvents.ts | 50 ++++++-- 3 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 src/app/(payload)/api/auth/login/route.ts diff --git a/src/app/(payload)/api/auth/login/route.ts b/src/app/(payload)/api/auth/login/route.ts new file mode 100644 index 0000000..dac3163 --- /dev/null +++ b/src/app/(payload)/api/auth/login/route.ts @@ -0,0 +1,134 @@ +/** + * Custom Login Endpoint mit Audit-Logging + * + * POST /api/auth/login + * + * Dieser Endpoint wrappet den nativen Payload-Login und loggt fehlgeschlagene + * Login-Versuche im Audit-Log. Erfolgreiche Logins werden durch den + * afterLogin-Hook in der Users-Collection geloggt. + * + * Body: + * - email: string (erforderlich) + * - password: string (erforderlich) + */ + +import { getPayload } from 'payload' +import configPromise from '@payload-config' +import { NextRequest, NextResponse } from 'next/server' +import { auditLoginFailed } from '@/hooks/auditAuthEvents' + +export async function POST(req: NextRequest): Promise { + try { + const payload = await getPayload({ config: configPromise }) + const body = await req.json() + + const { email, password } = body + + // Validierung + if (!email || !password) { + return NextResponse.json( + { error: 'E-Mail und Passwort sind erforderlich' }, + { status: 400 }, + ) + } + + try { + // Versuche Login über Payload + const result = await payload.login({ + collection: 'users', + data: { + email, + password, + }, + }) + + // Erfolgreicher Login - afterLogin Hook hat bereits geloggt + // Setze Cookie für die Session + const response = NextResponse.json({ + success: true, + user: { + id: result.user.id, + email: result.user.email, + }, + message: 'Login erfolgreich', + }) + + // Set the token cookie + if (result.token) { + response.cookies.set('payload-token', result.token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + // Token expiration (default 2 hours) + maxAge: result.exp ? result.exp - Math.floor(Date.now() / 1000) : 7200, + }) + } + + return response + } catch (loginError) { + // Login fehlgeschlagen - Audit-Log erstellen + const errorMessage = + loginError instanceof Error ? loginError.message : 'Unbekannter Fehler' + + // Bestimme den Grund für das Fehlschlagen + let reason = 'Unbekannter Fehler' + if (errorMessage.includes('not found') || errorMessage.includes('incorrect')) { + reason = 'Ungültige Anmeldedaten' + } else if (errorMessage.includes('locked')) { + reason = 'Konto gesperrt' + } else if (errorMessage.includes('disabled')) { + reason = 'Konto deaktiviert' + } else { + reason = errorMessage + } + + // Audit-Log für fehlgeschlagenen Login + await auditLoginFailed(payload, email, reason) + + return NextResponse.json( + { + success: false, + error: 'Anmeldung fehlgeschlagen', + // Keine detaillierten Infos aus Sicherheitsgründen + }, + { status: 401 }, + ) + } + } catch (error) { + console.error('[API:Auth] Login error:', error) + return NextResponse.json( + { error: 'Interner Serverfehler' }, + { status: 500 }, + ) + } +} + +/** + * GET /api/auth/login + * + * Gibt API-Dokumentation zurück. + */ +export async function GET(): Promise { + return NextResponse.json({ + endpoint: '/api/auth/login', + method: 'POST', + description: 'Login endpoint with audit logging for failed attempts', + body: { + email: 'string (required)', + password: 'string (required)', + }, + response: { + success: { + success: true, + user: { id: 'number', email: 'string' }, + message: 'string', + }, + error: { + success: false, + error: 'string', + }, + }, + note: 'Successful logins are logged via afterLogin hook. Failed attempts are logged here.', + }) +} diff --git a/src/collections/Users.ts b/src/collections/Users.ts index 9fe3061..643dace 100644 --- a/src/collections/Users.ts +++ b/src/collections/Users.ts @@ -1,6 +1,10 @@ import type { CollectionConfig } from 'payload' import { auditUserAfterChange, auditUserAfterDelete } from '../hooks/auditUserChanges' -import { auditAfterLogin, auditAfterLogout } from '../hooks/auditAuthEvents' +import { + auditAfterLogin, + auditAfterLogout, + auditAfterForgotPassword, +} from '../hooks/auditAuthEvents' export const Users: CollectionConfig = { slug: 'users', @@ -13,6 +17,7 @@ export const Users: CollectionConfig = { afterDelete: [auditUserAfterDelete], afterLogin: [auditAfterLogin], afterLogout: [auditAfterLogout], + afterForgotPassword: [auditAfterForgotPassword], }, fields: [ { diff --git a/src/hooks/auditAuthEvents.ts b/src/hooks/auditAuthEvents.ts index 010bc86..c46d70f 100644 --- a/src/hooks/auditAuthEvents.ts +++ b/src/hooks/auditAuthEvents.ts @@ -8,8 +8,10 @@ import type { CollectionAfterLoginHook, CollectionAfterLogoutHook, CollectionAfterForgotPasswordHook, + Payload, + PayloadRequest, } from 'payload' -import { logLoginSuccess, createAuditLog } from '../lib/audit/audit-service' +import { logLoginSuccess, logLoginFailed, createAuditLog } from '../lib/audit/audit-service' interface AuthUser { id: number @@ -56,23 +58,49 @@ export const auditAfterLogout: CollectionAfterLogoutHook = async ({ req }) => { /** * Hook: Loggt Passwort-Reset-Anfragen + * + * WICHTIG: Dieser Hook muss in Users.hooks.afterForgotPassword registriert werden */ -export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({ args, context }) => { - // Hinweis: Bei Forgot Password haben wir nur die E-Mail, nicht die User-ID - // aus Sicherheitsgründen wird nicht offengelegt ob die E-Mail existiert +export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({ + args, + context, +}) => { + // Bei Forgot Password haben wir nur die E-Mail, nicht die User-ID + // Aus Sicherheitsgründen wird nicht offengelegt ob die E-Mail existiert + const email = args?.data?.email as string | undefined - // Wir loggen nur wenn der Context die Payload-Instanz enthält - if (context && 'payload' in context) { - const payload = context.payload as typeof import('payload').default + // Payload-Instanz aus dem Context holen + const payload = (context as { req?: { payload?: Payload } })?.req?.payload - await createAuditLog(payload as unknown as import('payload').Payload, { + if (payload && email) { + await createAuditLog(payload, { action: 'password_reset', severity: 'info', entityType: 'users', - userEmail: args.data?.email as string || 'unknown', - description: `Passwort-Reset angefordert für ${args.data?.email || 'unknown'}`, + userEmail: email, + description: `Passwort-Reset angefordert für ${email}`, }) - console.log(`[Audit:Auth] Password reset requested for ${args.data?.email}`) + console.log(`[Audit:Auth] Password reset requested for ${email}`) } } + +/** + * Hilfsfunktion: Loggt fehlgeschlagene Logins + * + * Diese Funktion kann von Custom-Auth-Handlern oder Middleware aufgerufen werden, + * da Payload keinen nativen afterLoginFailed Hook hat. + * + * Verwendung: + * - In Custom-API-Routes wenn Login fehlschlägt + * - In Middleware die Auth-Errors abfängt + */ +export async function auditLoginFailed( + payload: Payload, + email: string, + reason: string, + req?: PayloadRequest, +): Promise { + await logLoginFailed(payload, email, reason, req) + console.log(`[Audit:Auth] Login failed for ${email}: ${reason}`) +}