cms.c2sgmbh/src/app/(payload)/api/auth/login/route.ts
Martin Porwoll e3987e50dc feat: security hardening, monitoring improvements, and API guards
- Hardened cron endpoints with coordination and auth improvements
- Added API guards and input validation layer
- Security observability and secrets health checks
- Monitoring types and service improvements
- PDF URL validation and newsletter unsubscribe security
- Unit tests for security-critical paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 11:42:56 +00:00

212 lines
5.8 KiB
TypeScript

/**
* 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.
*
* Security:
* - Rate-Limiting: 5 Versuche pro 15 Minuten pro IP (Anti-Brute-Force)
* - IP-Blocklist-Prüfung
* - Audit-Logging für fehlgeschlagene Logins
*
* Body:
* - email: string (erforderlich)
* - password: string (erforderlich)
*/
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import { NextRequest, NextResponse } from 'next/server'
import { logLoginFailed, logRateLimit } from '@/lib/audit/audit-service'
import {
authLimiter,
runApiGuards,
} from '@/lib/security'
import {
asObject,
requiredString,
validateJsonBody,
validationErrorResponse,
type ApiValidationResult,
} from '@/lib/validation'
/**
* Extrahiert Client-Informationen aus dem Request für Audit-Logging
*/
function getClientInfo(req: NextRequest, ipAddress?: string): { ipAddress: string; userAgent: string } {
const userAgent = req.headers.get('user-agent') || 'unknown'
return { ipAddress: ipAddress || 'unknown', userAgent }
}
interface LoginBody {
email: string
password: string
}
function validateLoginBody(input: unknown): ApiValidationResult<LoginBody> {
const objectResult = asObject(input)
if (!objectResult.valid) {
return objectResult as ApiValidationResult<LoginBody>
}
const emailResult = requiredString(objectResult.data, 'email')
const passwordResult = requiredString(objectResult.data, 'password')
const issues = [
...(emailResult.valid ? [] : emailResult.issues),
...(passwordResult.valid ? [] : passwordResult.issues),
]
if (issues.length > 0) {
return { valid: false, issues }
}
return {
valid: true,
data: {
email: emailResult.data,
password: passwordResult.data,
},
}
}
export async function POST(req: NextRequest): Promise<NextResponse> {
try {
const payload = await getPayload({ config: configPromise })
const guardResult = await runApiGuards(req, {
endpoint: '/api/auth/login',
blocklistOnly: true,
csrf: 'browser',
rateLimiter: authLimiter,
rateLimitMax: 5,
onRateLimit: async () => {
await logRateLimit(payload, '/api/auth/login', undefined, undefined)
},
})
if (!guardResult.ok) {
return guardResult.response
}
const bodyResult = await validateJsonBody(req, validateLoginBody)
if (!bodyResult.valid) {
return validationErrorResponse(bodyResult.issues)
}
const { email, password } = bodyResult.data
try {
// Versuche Login über Payload
const result = await payload.login({
collection: 'users',
data: {
email,
password,
},
})
if (!result.user) {
throw new Error('Login returned no user')
}
// 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 mit vollem Context
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 if (errorMessage.includes('verify')) {
reason = 'E-Mail nicht verifiziert'
} else {
reason = errorMessage
}
// Client-Info für Audit-Log extrahieren
const clientInfo = getClientInfo(req, guardResult.ip)
// Audit-Log für fehlgeschlagenen Login mit vollem Client-Context
await logLoginFailed(payload, email, reason, clientInfo)
console.log(
`[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`,
)
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<NextResponse> {
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.',
})
}