Modul-Review 1: Auth-Nachbesserungen (Sicherheit & UX)
Deploy App / deploy (push) Successful in 1m10s
Deploy App / deploy (push) Successful in 1m10s
Ergebnis der ersten Test- und Review-Runde zum Modul "Zugang & App-Rahmen": - Login-Timing-Ausgleich: unbekannter Benutzer wird gegen Dummy-bcrypt-Hash geprueft -> Antwortzeit verraet nicht mehr, ob ein Name existiert - Zurueck-Knopf nach Logout: pageshow-Waechter prueft die Session erneut und leitet die aus dem bfcache zurueckgeholte Ansicht auf /login - Rate-Limiting (neues lib/rate-limit.ts): Login 10/15min, Registrierung 5/h je IP, Passwortaenderung 10/15min je Benutzer; 429 + Retry-After - Passwort-Dialog laeuft neu ueber Modal -> schliesst auf Esc (Fokus-Falle, aria-modal inklusive) - Registrierungs-Fehler getrennt: nur belegter Name = 409 mit freundlicher Meldung, sonst 500 statt roher Prisma-Meldung SPEZIFIKATION 0.27 (3.1.2/3/4, neues 3.1.6). 261 -> 267 Tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
import { changeUserPassword } from "@/lib/users";
|
||||
import { rateLimit, tooManyRequests } from "@/lib/rate-limit";
|
||||
|
||||
const changePasswordSchema = z.object({
|
||||
currentPassword: z.string().min(1),
|
||||
@@ -14,6 +15,11 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: "Nicht authentifiziert." }, { status: 401 });
|
||||
}
|
||||
|
||||
// Bremse gegen das Durchprobieren des aktuellen Passworts: 10 Versuche je Benutzer und
|
||||
// 15 Minuten (nach dem Auth-Check, damit der Schlüssel die echte Benutzer-ID ist).
|
||||
const limited = rateLimit(`change-pw:${userId}`, 10, 15 * 60 * 1000);
|
||||
if (!limited.ok) return tooManyRequests(limited.retryAfterSeconds);
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = changePasswordSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { createSessionToken, SESSION_COOKIE_NAME } from "@/lib/auth";
|
||||
import { verifyUserCredentials } from "@/lib/users";
|
||||
import { clientIp, rateLimit, tooManyRequests } from "@/lib/rate-limit";
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
@@ -9,6 +10,10 @@ const loginSchema = z.object({
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// Bremse gegen Brute-Force: 10 Versuche je IP und 15 Minuten.
|
||||
const limited = rateLimit(`login:${clientIp(request)}`, 10, 15 * 60 * 1000);
|
||||
if (!limited.ok) return tooManyRequests(limited.retryAfterSeconds);
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = loginSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { createSessionToken, SESSION_COOKIE_NAME } from "@/lib/auth";
|
||||
import { registerUser, validateUsername } from "@/lib/users";
|
||||
import { registerUser, UsernameTakenError, validateUsername } from "@/lib/users";
|
||||
import { clientIp, rateLimit, tooManyRequests } from "@/lib/rate-limit";
|
||||
|
||||
const registerSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
@@ -9,6 +10,10 @@ const registerSchema = z.object({
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
// Bremse gegen Massen-Registrierung: 5 Konten je IP und Stunde.
|
||||
const limited = rateLimit(`register:${clientIp(request)}`, 5, 60 * 60 * 1000);
|
||||
if (!limited.ok) return tooManyRequests(limited.retryAfterSeconds);
|
||||
|
||||
const body = await request.json();
|
||||
const parsed = registerSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
@@ -25,10 +30,13 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
user = await registerUser(parsed.data.username, parsed.data.password);
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ error: e instanceof Error ? e.message : "Registrierung fehlgeschlagen." },
|
||||
{ status: 409 }
|
||||
);
|
||||
// Nur der "Name vergeben"-Fall ist ein 409 mit freundlicher Meldung; alles andere
|
||||
// (z. B. DB kurzzeitig weg) ist ein echter Serverfehler und darf nicht als Konflikt
|
||||
// mit roher Prisma-Meldung durchgereicht werden.
|
||||
if (e instanceof UsernameTakenError) {
|
||||
return NextResponse.json({ error: e.message }, { status: 409 });
|
||||
}
|
||||
return NextResponse.json({ error: "Registrierung fehlgeschlagen." }, { status: 500 });
|
||||
}
|
||||
|
||||
const token = await createSessionToken(user.id);
|
||||
|
||||
Reference in New Issue
Block a user