feat(auth): Anmeldung, Rollen und Benutzerverwaltung (#21)
OIDC mit Discovery, PKCE, state und nonce; lokaler Login mit argon2 als Notfallzugang; Rechte in einer Tabelle, Prüfung ausschließlich serverseitig, Rolle und Aktiv-Status pro Anfrage aus der Datenbank. Ein unabhängiger Review fand eine offene Weiterleitung: die Präfix-Prüfung des Rücksprungziels ließ /<TAB>/fremd.example durch, woraus der Browser //fremd.example macht. Das Ziel wird jetzt aufgelöst und die Herkunft verglichen. Dazu acht weitere Befunde, alle behoben. 274 Tests. Closes #7 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #21.
This commit is contained in:
+42
-4
@@ -1,15 +1,48 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { oeffentlicheAdresse } from "@/lib/adressen";
|
||||
import { SITZUNG_COOKIE } from "@/lib/sitzung";
|
||||
|
||||
/**
|
||||
* In Next 16 heisst die frueher middleware.ts genannte Datei proxy.ts und laeuft
|
||||
* in der Node-Laufzeit.
|
||||
*
|
||||
* Hier wird die Content-Security-Policy pro Anfrage mit einer Nonce gesetzt. Sie
|
||||
* muss dynamisch sein, deshalb steht sie nicht bei den statischen Kopfzeilen in
|
||||
* next.config.ts. Die Anmeldepflicht kommt in #007 dazu.
|
||||
* Sie tut zweierlei:
|
||||
*
|
||||
* 1. Content-Security-Policy mit einer Nonce je Anfrage. Die muss dynamisch
|
||||
* sein, deshalb steht sie nicht bei den festen Kopfzeilen in next.config.ts.
|
||||
* 2. Ein billiges Tor: ohne Sitzungscookie geht es gar nicht erst weiter, und
|
||||
* der Benutzer landet auf der Anmeldeseite – mit dem urspruenglichen Ziel im
|
||||
* Gepaeck. Geprueft wird das Cookie hier NICHT; das macht die Huelle unter
|
||||
* src/app/(app)/layout.tsx gegen die Datenbank. Diese Trennung spart bei
|
||||
* jeder Anfrage ohne Cookie eine Datenbankabfrage, ohne die Sicherheit an
|
||||
* den Proxy zu haengen.
|
||||
*/
|
||||
|
||||
/** Pfade, die ohne Anmeldung erreichbar sein muessen. */
|
||||
const OEFFENTLICH: readonly string[] = [
|
||||
"/anmelden",
|
||||
// Der Anmeldevorgang selbst – ohne ihn koennte sich niemand anmelden.
|
||||
"/api/auth",
|
||||
// Healthcheck fuer Docker und den Reverse Proxy.
|
||||
"/api/gesundheit",
|
||||
"/manifest.webmanifest",
|
||||
"/icon.svg",
|
||||
];
|
||||
|
||||
export function istOeffentlich(pfad: string): boolean {
|
||||
return OEFFENTLICH.some((eintrag) => pfad === eintrag || pfad.startsWith(`${eintrag}/`));
|
||||
}
|
||||
|
||||
export function proxy(anfrage: NextRequest): NextResponse {
|
||||
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
|
||||
const pfad = anfrage.nextUrl.pathname;
|
||||
|
||||
if (!istOeffentlich(pfad) && !anfrage.cookies.has(SITZUNG_COOKIE)) {
|
||||
const ziel = pfad + anfrage.nextUrl.search;
|
||||
const anmelden = oeffentlicheAdresse("/anmelden", anfrage.nextUrl.origin);
|
||||
if (ziel !== "/") anmelden.searchParams.set("ziel", ziel);
|
||||
return NextResponse.redirect(anmelden);
|
||||
}
|
||||
|
||||
const richtlinie = [
|
||||
`default-src 'self'`,
|
||||
@@ -29,6 +62,11 @@ export function proxy(anfrage: NextRequest): NextResponse {
|
||||
|
||||
const kopfzeilen = new Headers(anfrage.headers);
|
||||
kopfzeilen.set("x-nonce", nonce);
|
||||
// Server Components kennen den Pfad nicht von sich aus; die Huelle braucht ihn
|
||||
// fuer den aktiven Eintrag in der Navigation und fuer den Ruecksprung nach der
|
||||
// Anmeldung.
|
||||
kopfzeilen.set("x-pfad", pfad);
|
||||
kopfzeilen.set("x-abfrage", anfrage.nextUrl.search);
|
||||
|
||||
const antwort = NextResponse.next({ request: { headers: kopfzeilen } });
|
||||
antwort.headers.set("Content-Security-Policy", richtlinie);
|
||||
@@ -36,6 +74,6 @@ export function proxy(anfrage: NextRequest): NextResponse {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// Statische Dateien und Bilder brauchen keine CSP-Nonce.
|
||||
// Statische Dateien und Bilder brauchen weder CSP-Nonce noch Tor.
|
||||
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|webp|woff2?)$).*)"],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user