/** * POST /api/auth/login * * Login mit Rate-Limiting (plan.md Abschnitt 11). * Fehlermeldung ist bewusst generisch, um Account-Enumeration * zu verhindern. */ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db/client'; import { verifyPassword } from '@/lib/auth/password'; import { createSessionToken, setSessionCookie } from '@/lib/auth/session'; import { loginSchema } from '@/lib/validation'; import { errorResponse } from '@/lib/api/responses'; import { checkRateLimit, recordRateLimitHit, resetRateLimit, getClientIp, } from '@/lib/security/rate-limit'; /** * Maximal 5 Fehlversuche pro E-Mail und Zeitfenster (In-Memory, MVP). * Zusaetzlich ein IP-Limit: verhindert, dass ein Angreifer mit vielen * erfundenen E-Mails den Speicher flutet oder verteiltes Brute-Forcing * gegen ein einzelnes Konto betreibt. */ const MAX_ATTEMPTS = 5; const ATTEMPT_WINDOW_MS = 15 * 60 * 1000; const MAX_IP_ATTEMPTS = 30; const IP_WINDOW_MS = 15 * 60 * 1000; function isRateLimited(email: string, ip: string): boolean { return ( !checkRateLimit(`login:email:${email}`, MAX_ATTEMPTS, ATTEMPT_WINDOW_MS) .allowed || !checkRateLimit(`login:ip:${ip}`, MAX_IP_ATTEMPTS, IP_WINDOW_MS).allowed ); } function recordFailedAttempt(email: string, ip: string): void { recordRateLimitHit(`login:email:${email}`, ATTEMPT_WINDOW_MS); recordRateLimitHit(`login:ip:${ip}`, IP_WINDOW_MS); } export async function POST(request: NextRequest): Promise { try { const body = loginSchema.parse(await request.json()); const ip = getClientIp(request); if (isRateLimited(body.email, ip)) { return NextResponse.json( { error: 'Zu viele Anmeldeversuche. Bitte spaeter erneut.' }, { status: 429 }, ); } const user = await prisma.user.findUnique({ where: { email: body.email }, select: { id: true, email: true, role: true, passwordHash: true }, }); // Auch bei unbekannter E-Mail bcrypt ausfuehren (Timing-Abgleich). const passwordHash = user?.passwordHash ?? '$2a$12$C6UzMDM.H6dfI/f/IKcEeO7ZBpQqQqQqQqQqQqQqQqQqQqQqQqQqQ'; const passwordValid = await verifyPassword(body.password, passwordHash); if (!user || !passwordValid) { recordFailedAttempt(body.email, ip); return NextResponse.json( { error: 'E-Mail oder Passwort ist falsch.' }, { status: 401 }, ); } resetRateLimit(`login:email:${body.email}`); const token = await createSessionToken(user); await setSessionCookie(token); return NextResponse.json({ user: { id: user.id, email: user.email, role: user.role }, }); } catch (error) { return errorResponse(error); } }