feat(auth): Anmeldung mit Passwort und Passkey
Kern der Benutzerverwaltung. Passwoerter: - scrypt aus der Node-Standardbibliothek (N=32768, r=8, p=1), speicherhart und ohne zusaetzliches natives Modul - Format scrypt$N$r$p$salt$hash, damit sich die Parameter spaeter anheben lassen - fakeVerifyDelay verbrennt bei unbekanntem Benutzernamen dieselbe Rechenzeit, sonst liesse sich an der Antwortzeit ablesen, welche Konten existieren Passkeys ueber SimpleWebAuthn 13: - Anmeldung ohne Benutzernamen ueber auffindbare Credentials - Challenges liegen in der Datenbank und werden beim Einloesen geloescht, koennen also nicht erneut eingespielt werden - rpID und Origin kommen aus APP_URL Sitzungen: - Zufallstoken im HttpOnly-Cookie, in der Datenbank nur dessen Hash - Abmelden und Sperren wirken sofort, weil bei jeder Anfrage nachgeschlagen wird - anders als bei einem selbsttragenden Token Middleware: CSRF-Pruefung ueber Sec-Fetch-Site bzw. Origin, Sicherheitskopfzeilen inklusive CSP mit Per-Request-Nonce und strict-dynamic, dazu eine grobe Zugangsschranke. Letztere ist ausdruecklich keine Sicherheitsgrenze - im Edge-Runtime gibt es keine Datenbank. Verbindlich entscheiden requireUserPage und requireUserApi, serverseitig, in jeder Seite und jedem Endpunkt. config.ts liest APP_NAME, APP_TAGLINE und APP_URL zur Laufzeit statt ueber NEXT_PUBLIC_-Variablen. Die waeren beim Build fest ins Bundle geschrieben; so laesst sich dasselbe Docker-Image ohne Neubau umbenennen und unter einer anderen Adresse betreiben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSessionUser, type SessionUser } from "@/lib/auth/session";
|
||||
import { getInstanceState } from "@/lib/auth/settings";
|
||||
|
||||
/**
|
||||
* Zugriffsschutz.
|
||||
*
|
||||
* Die Middleware prueft nur, ob ueberhaupt ein Sitzungscookie mitkommt – im
|
||||
* Edge-Runtime steht keine Datenbank zur Verfuegung. Die eigentliche
|
||||
* Entscheidung faellt hier, serverseitig, und zwar in jeder Seite und jedem
|
||||
* API-Endpunkt. Das ist die Sicherheitsgrenze.
|
||||
*/
|
||||
|
||||
export type { SessionUser };
|
||||
|
||||
/** Fuer Server Components: leitet auf /login bzw. /setup um. */
|
||||
export async function requireUserPage(): Promise<SessionUser> {
|
||||
const user = await getSessionUser();
|
||||
if (user) return user;
|
||||
|
||||
const state = await getInstanceState();
|
||||
redirect(state.needsSetup ? "/setup" : "/login");
|
||||
}
|
||||
|
||||
/** Wie requireUserPage, verlangt zusaetzlich die Administratorrolle. */
|
||||
export async function requireAdminPage(): Promise<SessionUser> {
|
||||
const user = await requireUserPage();
|
||||
if (user.role !== "ADMIN") redirect("/");
|
||||
return user;
|
||||
}
|
||||
|
||||
export type ApiAuth =
|
||||
| { ok: true; user: SessionUser }
|
||||
| { ok: false; response: NextResponse };
|
||||
|
||||
/** Fuer Route Handler: liefert entweder den Benutzer oder eine 401/403-Antwort. */
|
||||
export async function requireUserApi(): Promise<ApiAuth> {
|
||||
const user = await getSessionUser();
|
||||
if (!user) {
|
||||
return {
|
||||
ok: false,
|
||||
response: NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 }),
|
||||
};
|
||||
}
|
||||
return { ok: true, user };
|
||||
}
|
||||
|
||||
export async function requireAdminApi(): Promise<ApiAuth> {
|
||||
const auth = await requireUserApi();
|
||||
if (!auth.ok) return auth;
|
||||
if (auth.user.role !== "ADMIN") {
|
||||
return {
|
||||
ok: false,
|
||||
response: NextResponse.json(
|
||||
{ error: "Diese Aktion ist Administratoren vorbehalten." },
|
||||
{ status: 403 },
|
||||
),
|
||||
};
|
||||
}
|
||||
return auth;
|
||||
}
|
||||
Reference in New Issue
Block a user