import { afterEach, describe, expect, it } from "vitest"; import type { Sitzung } from "@/lib/sitzung"; import { base32Dekodieren, base32Kodieren, totpCode, totpPruefen, totpSchritt } from "@/lib/totp"; import { sicheresZiel } from "@/server/auth/anmeldung"; import { rolleAusGruppen } from "@/server/auth/oidc"; import { BESTAETIGUNG_NOETIG, RECHTE, darf } from "@/server/auth/rechte"; import { BESTAETIGUNG_GUELTIG_MS, bestaetigungFrisch, sitzungBewerten } from "@/server/auth/sitzungspflicht"; import { OEFFENTLICH, istOeffentlich, sicherheitsrichtlinie } from "@/proxy"; /** Entscheidungen rund um Anmeldung und Rechte, die ohne Datenbank pruefbar sind (#004). */ const UMGEBUNG = { ...process.env }; afterEach(() => { process.env = { ...UMGEBUNG }; }); describe("TOTP nach RFC 6238", () => { // Testvektoren aus RFC 6238, Anhang B (SHA-1, Geheimnis "12345678901234567890"). const geheimnis = Buffer.from("12345678901234567890"); it.each([ [59, "94287082"], [1111111109, "07081804"], [1111111111, "14050471"], [1234567890, "89005924"], [2000000000, "69279037"], ])("Zeitpunkt %i ergibt %s", (sekunden, erwartet) => { expect(totpCode(geheimnis, totpSchritt(sekunden * 1000), 8)).toBe(erwartet); expect(totpCode(geheimnis, totpSchritt(sekunden * 1000))).toBe(erwartet.slice(2)); }); it("kodiert Base32 verlustfrei", () => { expect(base32Kodieren(geheimnis)).toBe("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"); expect(base32Dekodieren("gezd gnbv gy3t qojq gezd gnbv gy3t qojq")).toEqual(geheimnis); }); it("nimmt den Code des Nachbarschritts an, aber keinen zweimal", () => { const base32 = base32Kodieren(geheimnis); const jetzt = 1_111_111_111_000; const vorher = totpCode(geheimnis, totpSchritt(jetzt) - 1); const schritt = totpPruefen(base32, vorher, jetzt); expect(schritt).toBe(totpSchritt(jetzt) - 1); // Derselbe Code nach erfolgreicher Anmeldung: abgelehnt (Wiederholung). expect(totpPruefen(base32, vorher, jetzt, schritt)).toBeUndefined(); }); it("lehnt alte, kaputte und fremde Codes ab", () => { const base32 = base32Kodieren(geheimnis); const jetzt = 1_111_111_111_000; expect(totpPruefen(base32, totpCode(geheimnis, totpSchritt(jetzt) - 3), jetzt)).toBeUndefined(); expect(totpPruefen(base32, "12345", jetzt)).toBeUndefined(); expect(totpPruefen(base32, "abcdef", jetzt)).toBeUndefined(); }); }); describe("Rechte", () => { it("laesst Private Keys nur Admins ausrollen und anzeigen (Freigabe Frage 3)", () => { expect(darf("ADMIN", "privateKeyAusrollen")).toBe(true); expect(darf("OPERATOR", "privateKeyAusrollen")).toBe(false); expect(darf("OPERATOR", "privateKeyAnzeigen")).toBe(false); expect(darf("LESER", "privateKeyAnzeigen")).toBe(false); }); it("laesst Operatoren Public Keys ausrollen, Leser nicht", () => { expect(darf("OPERATOR", "publicKeyAusrollen")).toBe(true); expect(darf("LESER", "publicKeyAusrollen")).toBe(false); }); it("laesst jeden eigene Public Keys hinterlegen (Freigabe Frage 4)", () => { for (const rolle of ["ADMIN", "OPERATOR", "LESER"] as const) { expect(darf(rolle, "eigeneSchluesselHinterlegen")).toBe(true); } }); it("verlangt fuer kritische Aktionen eine Bestaetigung, und die nur fuer Admin-Rechte", () => { expect([...BESTAETIGUNG_NOETIG].sort()).toEqual( ["deploymentKeyVerwalten", "massenwiderruf", "privateKeyAnzeigen", "zielkontenVerwalten"].sort(), ); for (const recht of BESTAETIGUNG_NOETIG) { expect(RECHTE[recht]).toEqual(["ADMIN"]); } }); }); describe("Rollen aus OIDC-Gruppen", () => { it("bildet die Standardgruppen ab und nimmt die staerkste", () => { expect(rolleAusGruppen(["sshkv-readers"])).toBe("LESER"); expect(rolleAusGruppen(["sshkv-operators"])).toBe("OPERATOR"); expect(rolleAusGruppen(["sshkv-readers", "sshkv-admins"])).toBe("ADMIN"); }); it("gibt ohne passende Gruppe gar keine Rolle – kein stiller Lesezugriff", () => { expect(rolleAusGruppen([])).toBeUndefined(); expect(rolleAusGruppen(["andere", "SSHKV-ADMINS"])).toBeUndefined(); }); it("folgt umbenannten Gruppen aus der Umgebung", () => { process.env["SSHKV_OIDC_GROUP_ADMIN"] = "it-root"; expect(rolleAusGruppen(["it-root"])).toBe("ADMIN"); expect(rolleAusGruppen(["sshkv-admins"])).toBeUndefined(); }); }); describe("Sitzung bewerten", () => { const sitzung: Sitzung = { benutzerId: "b1", email: "a@b.de", name: "A", herkunft: "oidc", ausgestelltAm: 1_700_000_000, }; it("traegt fuer ein aktives Konto", () => { expect(sitzungBewerten(sitzung, { id: "b1", aktiv: true, sitzungenGueltigAb: new Date(0) })).toEqual({ gueltig: true, }); }); it("verfaellt fuer fremde, deaktivierte und widerrufene Konten", () => { expect(sitzungBewerten(sitzung, null).gueltig).toBe(false); expect(sitzungBewerten(sitzung, { id: "b2", aktiv: true, sitzungenGueltigAb: new Date(0) }).gueltig).toBe( false, ); expect( sitzungBewerten(sitzung, { id: "b1", aktiv: false, sitzungenGueltigAb: new Date(0) }).gueltig, ).toBe(false); const spaeter = new Date((sitzung.ausgestelltAm + 60) * 1000); expect(sitzungBewerten(sitzung, { id: "b1", aktiv: true, sitzungenGueltigAb: spaeter })).toEqual({ gueltig: false, grund: "widerrufen", }); }); it("haelt eine Bestaetigung genau fuenf Minuten fuer frisch", () => { const jetzt = new Date("2026-10-12T10:00:00Z"); expect(bestaetigungFrisch(null, jetzt)).toBe(false); expect(bestaetigungFrisch(new Date(jetzt.getTime() - BESTAETIGUNG_GUELTIG_MS), jetzt)).toBe(true); expect(bestaetigungFrisch(new Date(jetzt.getTime() - BESTAETIGUNG_GUELTIG_MS - 1), jetzt)).toBe(false); // Aus der Zukunft zaehlt nicht (Uhr verstellt oder Wert manipuliert). expect(bestaetigungFrisch(new Date(jetzt.getTime() + 60_000), jetzt)).toBe(false); }); }); describe("Ruecksprungziel", () => { it("nimmt Pfade der Anwendung an und weist alles andere ab", () => { expect(sicheresZiel("/schluessel/abc?tab=2")).toBe("/schluessel/abc?tab=2"); for (const boesartig of [ "https://fremd.example", "//fremd.example", "/\\fremd.example", "javascript:alert(1)", ]) { expect(sicheresZiel(boesartig)).toBe("/"); } expect(sicheresZiel(null)).toBe("/"); }); }); describe("Proxy", () => { it("haelt die oeffentlichen Pfade klein und benannt", () => { expect([...OEFFENTLICH].sort()).toEqual( ["/anmelden", "/api/auth", "/api/gesundheit", "/brand", "/icons", "/manifest.webmanifest"].sort(), ); expect(istOeffentlich("/api/auth/oidc/start")).toBe(true); expect(istOeffentlich("/anmeldenX")).toBe(false); expect(istOeffentlich("/schluessel")).toBe(false); }); it("liefert im Betrieb eine strenge CSP ohne unsafe-eval", () => { const richtlinie = sicherheitsrichtlinie("abc", false); expect(richtlinie).toContain("script-src 'self' 'nonce-abc' 'strict-dynamic'"); expect(richtlinie).not.toContain("unsafe-eval"); expect(richtlinie).toContain("frame-ancestors 'none'"); }); });