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:
@@ -0,0 +1,14 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { oeffentlicheAdresse } from "@/lib/adressen";
|
||||
import { sitzungBeenden } from "@/server/auth/anmeldung";
|
||||
|
||||
/**
|
||||
* Abmelden. Nur per POST, damit ein fremder Link oder ein vorgeladenes Bild
|
||||
* niemanden abmelden kann.
|
||||
*/
|
||||
export async function POST(anfrage: NextRequest): Promise<Response> {
|
||||
await sitzungBeenden();
|
||||
return NextResponse.redirect(oeffentlicheAdresse("/anmelden?abgemeldet=1", anfrage.nextUrl.origin), {
|
||||
status: 303,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { oeffentlicheAdresse } from "@/lib/adressen";
|
||||
import { sicheresZiel, sitzungSetzen } from "@/server/auth/anmeldung";
|
||||
import { benutzerAusOidcUebernehmen } from "@/server/auth/benutzer";
|
||||
import { OIDC_FLUSS_COOKIE, anmeldungAbschliessen } from "@/server/auth/oidc";
|
||||
|
||||
/**
|
||||
* Rueckruf des OIDC-Anbieters. Prueft den Fluss, uebernimmt den Benutzer und
|
||||
* setzt die Sitzung.
|
||||
*/
|
||||
export async function GET(anfrage: NextRequest): Promise<Response> {
|
||||
const flussToken = anfrage.cookies.get(OIDC_FLUSS_COOKIE)?.value;
|
||||
|
||||
try {
|
||||
// Die Adresse fuer die Pruefung muss die oeffentliche sein – der Anbieter
|
||||
// hat gegen die registrierte Rueckruf-Adresse signiert.
|
||||
const aktuelleAdresse = oeffentlicheAdresse(
|
||||
anfrage.nextUrl.pathname + anfrage.nextUrl.search,
|
||||
anfrage.nextUrl.origin,
|
||||
);
|
||||
|
||||
const anspruecke = await anmeldungAbschliessen(aktuelleAdresse, flussToken);
|
||||
const benutzer = await benutzerAusOidcUebernehmen(anspruecke);
|
||||
await sitzungSetzen(benutzer, "oidc");
|
||||
|
||||
const antwort = NextResponse.redirect(
|
||||
oeffentlicheAdresse(sicheresZiel(anspruecke.ziel), anfrage.nextUrl.origin),
|
||||
);
|
||||
antwort.cookies.delete(OIDC_FLUSS_COOKIE);
|
||||
return antwort;
|
||||
} catch (fehler) {
|
||||
// Nur Name und Meldung: die cause-Kette von openid-client kann
|
||||
// Antworten des Anbieters mitfuehren.
|
||||
console.error(
|
||||
`OIDC-Rueckruf fehlgeschlagen:`,
|
||||
fehler instanceof Error ? `${fehler.name}: ${fehler.message}` : String(fehler),
|
||||
);
|
||||
const antwort = NextResponse.redirect(
|
||||
oeffentlicheAdresse("/anmelden?fehler=oidc-rueckruf", anfrage.nextUrl.origin),
|
||||
);
|
||||
antwort.cookies.delete(OIDC_FLUSS_COOKIE);
|
||||
return antwort;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { oeffentlicheAdresse } from "@/lib/adressen";
|
||||
import { sicheresZiel, ueberHttps } from "@/server/auth/anmeldung";
|
||||
import { OIDC_FLUSS_COOKIE, anmeldungStarten, oidcAktiv } from "@/server/auth/oidc";
|
||||
|
||||
/** Beginnt die Anmeldung beim OIDC-Anbieter und leitet den Browser dorthin. */
|
||||
export async function GET(anfrage: NextRequest): Promise<Response> {
|
||||
if (!oidcAktiv()) {
|
||||
return NextResponse.redirect(oeffentlicheAdresse("/anmelden?fehler=oidc-aus", anfrage.nextUrl.origin));
|
||||
}
|
||||
|
||||
const ziel = sicheresZiel(anfrage.nextUrl.searchParams.get("ziel"));
|
||||
try {
|
||||
const { adresse, flussToken } = await anmeldungStarten(ziel);
|
||||
const antwort = NextResponse.redirect(adresse);
|
||||
antwort.cookies.set(OIDC_FLUSS_COOKIE, flussToken, {
|
||||
httpOnly: true,
|
||||
// Der Rueckruf kommt als Navigation vom Anbieter zurueck, also lax.
|
||||
sameSite: "lax",
|
||||
// Das Schema der eigenen Anwendung, nicht das des Anbieters: laeuft
|
||||
// authentik auf https und Laufzettel im Intranet auf http, schickte der
|
||||
// Browser ein Secure-Cookie nie zurueck und der Rueckruf scheiterte
|
||||
// reproduzierbar mit "Anmeldevorgang abgelaufen".
|
||||
secure: ueberHttps(),
|
||||
path: "/api/auth/oidc",
|
||||
maxAge: 600,
|
||||
});
|
||||
return antwort;
|
||||
} catch (fehler) {
|
||||
// Nur Name und Meldung: die cause-Kette von openid-client kann
|
||||
// Antworten des Anbieters mitfuehren.
|
||||
console.error(
|
||||
`OIDC-Anmeldung konnte nicht begonnen werden:`,
|
||||
fehler instanceof Error ? `${fehler.name}: ${fehler.message}` : String(fehler),
|
||||
);
|
||||
return NextResponse.redirect(oeffentlicheAdresse("/anmelden?fehler=oidc-start", anfrage.nextUrl.origin));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user