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:
2026-10-12 09:17:44 +02:00
co-authored by Claude Opus 5.5
parent 97e179d068
commit 5ef90310e1
20 changed files with 2320 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
import type { Prisma } from "@/generated/prisma/client";
import type { ProtokollErgebnis } from "@/generated/prisma/enums";
import { prisma } from "@/lib/db";
/**
* Schreibt einen Eintrag ins revisionssichere Protokoll (#005).
*
* Hash und Verkettung berechnet die Datenbank (Trigger), hier wird nur eingefuegt.
* `details` laeuft vor dem Schreiben durch `schwaerzen()` (#006), damit kein Private Key
* und kein Passwort hineingeraet – auch nicht aus Versehen ueber ein Fehlerobjekt.
*/
export type ProtokollEintrag = {
readonly akteur: { readonly id: string; readonly name: string } | "System";
readonly aktion: string;
readonly ergebnis: ProtokollErgebnis;
readonly hostName?: string | null;
readonly zielbenutzer?: string | null;
readonly fingerprint?: string | null;
readonly diff?: string | null;
readonly details?: Record<string, unknown>;
};
export async function protokollieren(
eintrag: ProtokollEintrag,
client: Prisma.TransactionClient = prisma,
): Promise<void> {
const akteur = eintrag.akteur === "System" ? { id: null, name: "System" } : eintrag.akteur;
await client.protokoll.create({
data: {
akteurId: akteur.id,
akteurName: akteur.name,
aktion: eintrag.aktion,
ergebnis: eintrag.ergebnis,
hostName: eintrag.hostName ?? null,
zielbenutzer: eintrag.zielbenutzer ?? null,
fingerprint: eintrag.fingerprint ?? null,
diff: eintrag.diff ?? null,
details: (eintrag.details as Prisma.InputJsonValue | undefined) ?? undefined,
},
});
}