import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { oeffentlicheAdresse } from "@/lib/adressen"; import { sicheresZiel } from "@/server/auth/anmeldung"; import { sitzungBewerten } from "@/server/auth/sitzungspflicht"; import type { Sitzung } from "@/lib/sitzung"; import { kundenkuerzelAusGruppen, oidcAktiv, oidcName, rolleAusGruppen, rueckrufAdresse, } from "@/server/auth/oidc"; /** * Die Entscheidungen rund um die Anmeldung, die ohne Datenbank und ohne Next * pruefbar sind (#007). */ const UMGEBUNG = { ...process.env }; afterEach(() => { process.env = { ...UMGEBUNG }; }); describe("Ruecksprungziel", () => { it("nimmt einen Pfad innerhalb der Anwendung an", () => { expect(sicheresZiel("/t/LZ-7K3F9Q")).toBe("/t/LZ-7K3F9Q"); expect(sicheresZiel("/admin/benutzer?seite=2")).toBe("/admin/benutzer?seite=2"); }); it("weist eine Weiterleitung nach draussen ab", () => { // Ohne diese Pruefung waere ein Anmeldelink verschickbar, der nach // erfolgreicher Anmeldung auf einer fremden Seite landet. for (const boesartig of [ "https://fremd.example/phishing", "//fremd.example", "/\\fremd.example", "http://fremd.example", "javascript:alert(1)", ]) { expect(sicheresZiel(boesartig)).toBe("/"); } }); it("faellt ohne Angabe auf die Startseite zurueck", () => { expect(sicheresZiel(undefined)).toBe("/"); expect(sicheresZiel(null)).toBe("/"); expect(sicheresZiel("")).toBe("/"); }); }); describe("Rollen aus OIDC-Gruppen", () => { beforeEach(() => { process.env["OIDC_GROUP_ADMIN"] = "lz-admin"; process.env["OIDC_GROUP_EDITOR"] = "lz-redakteur"; process.env["OIDC_GROUP_TECHNICIAN"] = "lz-techniker"; process.env["OIDC_GROUP_VIEWER"] = "lz-leser"; }); it("bildet jede Gruppe auf ihre Rolle ab", () => { expect(rolleAusGruppen(["lz-admin"])).toBe("ADMIN"); expect(rolleAusGruppen(["lz-redakteur"])).toBe("REDAKTEUR"); expect(rolleAusGruppen(["lz-techniker"])).toBe("TECHNIKER"); expect(rolleAusGruppen(["lz-leser"])).toBe("LESER"); }); it("gibt bei mehreren Gruppen die staerkste Rolle", () => { expect(rolleAusGruppen(["lz-leser", "lz-admin", "lz-techniker"])).toBe("ADMIN"); expect(rolleAusGruppen(["lz-techniker", "lz-redakteur"])).toBe("REDAKTEUR"); }); it("gibt ohne Treffer LESER, nie ADMIN und nie einen Fehler", () => { expect(rolleAusGruppen([])).toBe("LESER"); expect(rolleAusGruppen(["irgendwas", "ganz-anderes"])).toBe("LESER"); }); it("achtet auf Gross- und Kleinschreibung der Gruppennamen", () => { // Verzeichnisse unterscheiden hier, also darf die Zuordnung es auch. expect(rolleAusGruppen(["LZ-ADMIN"])).toBe("LESER"); }); it("ordnet nichts zu, wenn die Variablen fehlen", () => { process.env = { ...UMGEBUNG }; delete process.env["OIDC_GROUP_ADMIN"]; expect(rolleAusGruppen(["lz-admin"])).toBe("LESER"); }); }); describe("Kundenbindung aus OIDC-Gruppen", () => { it("liefert ohne Praefix nichts, damit keine Einschraenkung entsteht", () => { delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"]; expect(kundenkuerzelAusGruppen(["lz-kunde-MUS"])).toEqual([]); }); it("liest die Kuerzel hinter dem Praefix", () => { process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-"; expect(kundenkuerzelAusGruppen(["lz-kunde-MUS", "lz-kunde-rei", "lz-admin"])).toEqual(["MUS", "REI"]); }); it("verwirft Kuerzel, die nicht zur Form passen", () => { process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-"; // Dieselbe Form wie die Bedingung Kunde_kuerzel_form in der Datenbank. expect(kundenkuerzelAusGruppen(["lz-kunde-X", "lz-kunde-ZULANG7", "lz-kunde-MU_S"])).toEqual([]); }); }); describe("OIDC-Konfiguration", () => { it("gilt erst als eingerichtet, wenn alle drei Angaben da sind", () => { process.env["OIDC_ISSUER"] = "https://authentik.example/application/o/laufzettel/"; process.env["OIDC_CLIENT_ID"] = "kennung"; delete process.env["OIDC_CLIENT_SECRET"]; expect(oidcAktiv()).toBe(false); process.env["OIDC_CLIENT_SECRET"] = "geheim"; expect(oidcAktiv()).toBe(true); }); it("leitet die Rueckruf-Adresse aus APP_URL ab, nicht aus der Anfrage", () => { // Hinter einem Reverse Proxy traegt die Anfrage den internen Host; die beim // Anbieter hinterlegte Adresse ist aber die oeffentliche. process.env["APP_URL"] = "https://laufzettel.example.de"; expect(rueckrufAdresse()).toBe("https://laufzettel.example.de/api/auth/oidc/rueckruf"); }); it("wirft mit klarer Meldung, wenn APP_URL fehlt", () => { delete process.env["APP_URL"]; expect(() => rueckrufAdresse()).toThrow(/APP_URL/); }); it("nennt den Anbieter beim Namen, sonst neutral", () => { process.env["OIDC_NAME"] = "authentik"; expect(oidcName()).toBe("authentik"); delete process.env["OIDC_NAME"]; expect(oidcName()).toBe("Single Sign-on"); }); }); describe("Gueltigkeit einer Sitzung", () => { const jetzt = Math.floor(Date.now() / 1000); const sitzung: Sitzung = { benutzerId: "benutzer-1", email: "techniker@firma.example", name: "Techniker", herkunft: "passwort", ausgestelltAm: jetzt, }; it("traegt bei aktivem Benutzer", () => { expect(sitzungBewerten(sitzung, { id: "benutzer-1", aktiv: true, sitzungenUngueltigAb: null })).toEqual({ gueltig: true, }); }); it("faellt, wenn es den Benutzer nicht mehr gibt", () => { expect(sitzungBewerten(sitzung, null)).toEqual({ gueltig: false, grund: "unbekannt" }); }); it("faellt, wenn das Cookie auf einen anderen Benutzer zeigt", () => { expect(sitzungBewerten(sitzung, { id: "benutzer-2", aktiv: true, sitzungenUngueltigAb: null })).toEqual({ gueltig: false, grund: "unbekannt", }); }); it("faellt sofort bei einem deaktivierten Konto", () => { // Genau dafuer wird pro Anfrage aus der Datenbank gelesen: sonst liefe das // Cookie noch bis zu zwoelf Stunden weiter. expect(sitzungBewerten(sitzung, { id: "benutzer-1", aktiv: false, sitzungenUngueltigAb: null })).toEqual({ gueltig: false, grund: "deaktiviert", }); }); it("faellt, wenn die Sitzung vor dem Widerruf ausgestellt wurde", () => { expect( sitzungBewerten(sitzung, { id: "benutzer-1", aktiv: true, sitzungenUngueltigAb: new Date((jetzt + 60) * 1000), }), ).toEqual({ gueltig: false, grund: "widerrufen" }); }); it("traegt, wenn die Sitzung nach dem Widerruf ausgestellt wurde", () => { expect( sitzungBewerten(sitzung, { id: "benutzer-1", aktiv: true, sitzungenUngueltigAb: new Date((jetzt - 60) * 1000), }), ).toEqual({ gueltig: true }); }); }); describe("Oeffentliche Adressen", () => { it("baut Umleitungen aus APP_URL, nicht aus der Anfrage", () => { // Hinter einem Reverse Proxy traegt die Anfrage den internen Host. Eine // daraus gebaute Umleitung schickt den Benutzer an eine Adresse, die er gar // nicht erreicht. process.env["APP_URL"] = "https://laufzettel.example.de"; expect(oeffentlicheAdresse("/anmelden", "http://10.0.0.7:3000").toString()).toBe( "https://laufzettel.example.de/anmelden", ); }); it("behaelt Abfrageparameter", () => { process.env["APP_URL"] = "https://laufzettel.example.de"; expect(oeffentlicheAdresse("/anmelden?fehler=oidc-aus", "http://intern").search).toBe("?fehler=oidc-aus"); }); it("faellt ohne APP_URL auf die Herkunft der Anfrage zurueck", () => { delete process.env["APP_URL"]; expect(oeffentlicheAdresse("/anmelden", "http://127.0.0.1:3100").toString()).toBe( "http://127.0.0.1:3100/anmelden", ); }); it("faellt auch bei unbrauchbarer APP_URL zurueck, statt stehen zu bleiben", () => { process.env["APP_URL"] = "keine-adresse"; expect(oeffentlicheAdresse("/anmelden", "http://127.0.0.1:3100").host).toBe("127.0.0.1:3100"); }); });