feat(auth): Anmeldung per OIDC und Notfall-Admin mit TOTP (#004)
Sitzung, OIDC-Fluss und Rueckruf aus Laufzettel uebernommen und auf sshkv angepasst. Abweichungen mit Absicht: - Ohne passende sshkv-Gruppe gibt es keinen Zugang, auch keinen Lesezugriff: schon die Hostliste verraet, wer wohin darf. - Notfall-Admin nur mit TOTP (Freigabe Frage 5). Ein richtiges Passwort fuehrt beim ersten Mal nur zur Einrichtung. Jeder Code gilt einmal, fuenf Fehlversuche sperren 15 Minuten. - Kritische Rechte verlangen eine Bestaetigung, die hoechstens fuenf Minuten alt ist. Bei OIDC mit prompt=login, max_age=0 und Pruefung von auth_time, sonst zaehlte eine laufende SSO-Sitzung als Beweis. - Sitzungen laufen acht statt zwoelf Stunden. Die Seiten folgen, sobald das Design-System (#002) steht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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,48 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { oeffentlicheAdresse } from "@/lib/adressen";
|
||||
import { sicheresZiel, sitzungSetzen } from "@/server/auth/anmeldung";
|
||||
import { OIDC_FLUSS_COOKIE, anmeldungAbschliessen } from "@/server/auth/oidc";
|
||||
import { angemeldet } from "@/server/auth/sitzungspflicht";
|
||||
import {
|
||||
AnmeldeFehler,
|
||||
oidcBenutzerUebernehmen,
|
||||
oidcBestaetigungVermerken,
|
||||
} from "@/server/services/anmeldung";
|
||||
|
||||
/**
|
||||
* Rueckruf des OIDC-Anbieters: entweder Anmeldung (Sitzung setzen) oder erneute
|
||||
* Bestaetigung des bereits angemeldeten Kontos (#004).
|
||||
*/
|
||||
export async function GET(anfrage: NextRequest): Promise<Response> {
|
||||
const flussToken = anfrage.cookies.get(OIDC_FLUSS_COOKIE)?.value;
|
||||
const ursprung = anfrage.nextUrl.origin;
|
||||
|
||||
try {
|
||||
// Gepruefte Adresse muss die oeffentliche sein – dagegen hat der Anbieter signiert.
|
||||
const aktuelleAdresse = oeffentlicheAdresse(anfrage.nextUrl.pathname + anfrage.nextUrl.search, ursprung);
|
||||
const anspruecke = await anmeldungAbschliessen(aktuelleAdresse, flussToken);
|
||||
|
||||
if (anspruecke.erneut) {
|
||||
const vorhanden = await angemeldet();
|
||||
if (!vorhanden) throw new AnmeldeFehler("Für eine Bestätigung muss eine Sitzung bestehen.");
|
||||
await oidcBestaetigungVermerken(vorhanden.benutzer.id, anspruecke);
|
||||
} else {
|
||||
const benutzer = await oidcBenutzerUebernehmen(anspruecke);
|
||||
await sitzungSetzen(benutzer, "oidc");
|
||||
}
|
||||
|
||||
const antwort = NextResponse.redirect(oeffentlicheAdresse(sicheresZiel(anspruecke.ziel), ursprung));
|
||||
antwort.cookies.delete(OIDC_FLUSS_COOKIE);
|
||||
return antwort;
|
||||
} catch (fehler) {
|
||||
console.error(
|
||||
"OIDC-Rueckruf fehlgeschlagen:",
|
||||
fehler instanceof Error ? `${fehler.name}: ${fehler.message}` : String(fehler),
|
||||
);
|
||||
// Nur ein Code geht in die Adresse, nie der Meldungstext.
|
||||
const code = fehler instanceof AnmeldeFehler ? fehler.grund : "oidc-rueckruf";
|
||||
const antwort = NextResponse.redirect(oeffentlicheAdresse(`/anmelden?fehler=${code}`, ursprung));
|
||||
antwort.cookies.delete(OIDC_FLUSS_COOKIE);
|
||||
return antwort;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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. Mit `?erneut=1` wird daraus die erneute
|
||||
* Bestaetigung vor einer kritischen Aktion (prompt=login, max_age=0) – siehe oidc.ts.
|
||||
*/
|
||||
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"));
|
||||
const erneut = anfrage.nextUrl.searchParams.get("erneut") === "1";
|
||||
try {
|
||||
const { adresse, flussToken } = await anmeldungStarten(ziel, erneut);
|
||||
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",
|
||||
// Massgeblich ist das Schema von sshkv, nicht das des Anbieters (sonst kaeme ein
|
||||
// Secure-Cookie ueber http nie zurueck).
|
||||
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