feat(anmeldung): lokale Benutzerkonten mit Pflicht-TOTP (#035)
Ohne authentik gab es nur den Notfall-Admin – ein geteiltes Konto ohne Rollen und ohne erkennbare Akteure im Audit-Log. Admins legen jetzt lokale Konten an; sshkv erzeugt ein Einmal-Passwort, das bei der ersten Anmeldung ersetzt wird, danach ist TOTP Pflicht. Rollen, Sperren (beendet Sitzungen sofort) und Zurücksetzen unter Einstellungen → Benutzer, nur für Admins mit frischer Bestätigung. SSHKV_LOKALE_KONTEN=0 schaltet lokale Konten ab; der Notfall-Admin bleibt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { passwortHashen } from "@/lib/passwort-hash";
|
||||
import { base32Dekodieren, totpCode, totpGeheimnisErzeugen, totpSchritt } from "@/lib/totp";
|
||||
import {
|
||||
AnmeldeFehler,
|
||||
notfallAnmelden,
|
||||
kontoAnmelden,
|
||||
oidcBenutzerUebernehmen,
|
||||
totpEinrichtungAbschliessen,
|
||||
} from "@/server/services/anmeldung";
|
||||
@@ -37,7 +37,7 @@ beforeEach(async () => {
|
||||
describe("Notfall-Admin", () => {
|
||||
it("fuehrt beim ersten Mal zur TOTP-Einrichtung, nicht in die Anwendung", async () => {
|
||||
await notfallAnlegen();
|
||||
const ergebnis = await notfallAnmelden("notfall", PASSWORT, "");
|
||||
const ergebnis = await kontoAnmelden("notfall", PASSWORT, "");
|
||||
expect(ergebnis.art).toBe("totpEinrichten");
|
||||
});
|
||||
|
||||
@@ -56,33 +56,33 @@ describe("Notfall-Admin", () => {
|
||||
expect(Buffer.from(gespeichert.totpGeheimnis ?? []).toString("utf8")).not.toContain(geheimnis);
|
||||
|
||||
const t1 = new Date(t0.getTime() + 60_000);
|
||||
await expect(notfallAnmelden("notfall", PASSWORT, "123456", t1)).rejects.toThrow(AnmeldeFehler);
|
||||
const ergebnis = await notfallAnmelden("notfall", PASSWORT, codeZu(geheimnis, t1), t1);
|
||||
await expect(kontoAnmelden("notfall", PASSWORT, "123456", t1)).rejects.toThrow(AnmeldeFehler);
|
||||
const ergebnis = await kontoAnmelden("notfall", PASSWORT, codeZu(geheimnis, t1), t1);
|
||||
expect(ergebnis.art).toBe("angemeldet");
|
||||
// Derselbe Code ein zweites Mal: abgelehnt.
|
||||
await expect(notfallAnmelden("notfall", PASSWORT, codeZu(geheimnis, t1), t1)).rejects.toThrow(
|
||||
await expect(kontoAnmelden("notfall", PASSWORT, codeZu(geheimnis, t1), t1)).rejects.toThrow(
|
||||
AnmeldeFehler,
|
||||
);
|
||||
});
|
||||
|
||||
it("verraet nicht, ob Name oder Passwort falsch war", async () => {
|
||||
await notfallAnlegen();
|
||||
const a = await notfallAnmelden("gibtsnicht", PASSWORT, "").catch((f: Error) => f.message);
|
||||
const b = await notfallAnmelden("notfall", "falsch-falsch-falsch", "").catch((f: Error) => f.message);
|
||||
const a = await kontoAnmelden("gibtsnicht", PASSWORT, "").catch((f: Error) => f.message);
|
||||
const b = await kontoAnmelden("notfall", "falsch-falsch-falsch", "").catch((f: Error) => f.message);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it("sperrt nach fuenf Fehlversuchen, auch fuer das richtige Passwort", async () => {
|
||||
await notfallAnlegen();
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
await expect(notfallAnmelden("notfall", "falsch-falsch-falsch", "")).rejects.toThrow();
|
||||
await expect(kontoAnmelden("notfall", "falsch-falsch-falsch", "")).rejects.toThrow();
|
||||
}
|
||||
await expect(notfallAnmelden("notfall", PASSWORT, "")).rejects.toThrow(/Zu viele Fehlversuche/);
|
||||
await expect(kontoAnmelden("notfall", PASSWORT, "")).rejects.toThrow(/Zu viele Fehlversuche/);
|
||||
});
|
||||
|
||||
it("protokolliert Fehlversuche ohne Passwort", async () => {
|
||||
await notfallAnlegen();
|
||||
await notfallAnmelden("notfall", "geheimes-falsches-pw", "").catch(() => undefined);
|
||||
await kontoAnmelden("notfall", "geheimes-falsches-pw", "").catch(() => undefined);
|
||||
const eintraege = await prisma.protokoll.findMany();
|
||||
expect(eintraege.map((e) => e.aktion)).toContain("anmeldung.notfall.fehlgeschlagen");
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user