Das Cookie erhielt im Produktionsbau immer Secure und wurde ueber http vom Browser verworfen – nach der Anmeldung landete jeder Klick wieder auf der Anmeldeseite. Massgeblich ist jetzt das Schema in APP_URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
168 lines
5.8 KiB
TypeScript
168 lines
5.8 KiB
TypeScript
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
|
import { hash } from "@node-rs/argon2";
|
|
import { cookieOptionen, sitzungErzeugen, sitzungLesen } from "@/lib/sitzung";
|
|
import { AnmeldeFehler, anmeldeRateLimit, mitOidcAnmelden, mitPasswortAnmelden } from "@/server/auth/benutzer";
|
|
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
|
|
|
const GEHEIMNIS = "test-geheimnis-mit-mindestens-32-zeichen!!";
|
|
|
|
describe("Sitzungen", () => {
|
|
beforeEach(() => {
|
|
process.env.APP_SECRET = GEHEIMNIS;
|
|
});
|
|
|
|
it("erzeugt ein Token und liest es wieder", async () => {
|
|
const token = await sitzungErzeugen({
|
|
benutzerId: "b1",
|
|
email: "admin@example.de",
|
|
name: "Florian",
|
|
herkunft: "passwort",
|
|
});
|
|
|
|
const sitzung = await sitzungLesen(token);
|
|
expect(sitzung).toEqual({
|
|
benutzerId: "b1",
|
|
email: "admin@example.de",
|
|
name: "Florian",
|
|
herkunft: "passwort",
|
|
});
|
|
});
|
|
|
|
it("weist ein verändertes oder fremd signiertes Token ab", async () => {
|
|
const token = await sitzungErzeugen({
|
|
benutzerId: "b1",
|
|
email: "admin@example.de",
|
|
name: "Florian",
|
|
herkunft: "passwort",
|
|
});
|
|
|
|
expect(await sitzungLesen(`${token}x`)).toBeNull();
|
|
|
|
process.env.APP_SECRET = "ein-ganz-anderes-geheimnis-mit-32-zeichen!";
|
|
expect(await sitzungLesen(token)).toBeNull();
|
|
});
|
|
|
|
it("gibt bei fehlendem Token null zurück", async () => {
|
|
expect(await sitzungLesen(undefined)).toBeNull();
|
|
expect(await sitzungLesen("")).toBeNull();
|
|
});
|
|
|
|
it("verlangt einen ausreichend langen Schlüssel", async () => {
|
|
process.env.APP_SECRET = "zu-kurz";
|
|
await expect(
|
|
sitzungErzeugen({ benutzerId: "b1", email: "a@b.de", name: "A", herkunft: "passwort" }),
|
|
).rejects.toThrow(/APP_SECRET/);
|
|
});
|
|
|
|
it("setzt Secure nur, wenn das Dashboard über HTTPS läuft", () => {
|
|
const vorher = process.env.APP_URL;
|
|
|
|
process.env.APP_URL = "https://proxledger.example.de";
|
|
expect(cookieOptionen().secure).toBe(true);
|
|
|
|
// Produktiv gebaut, aber über http erreichbar: sonst verwirft der Browser das Cookie
|
|
// und die Anmeldung landet sofort wieder auf der Anmeldeseite.
|
|
process.env.APP_URL = "http://192.168.1.50:3000";
|
|
expect(cookieOptionen().secure).toBe(false);
|
|
|
|
process.env.APP_URL = vorher;
|
|
expect(cookieOptionen().httpOnly).toBe(true);
|
|
expect(cookieOptionen().sameSite).toBe("lax");
|
|
});
|
|
});
|
|
|
|
describe.runIf(datenbankVerfuegbar)("Anmeldung", () => {
|
|
beforeEach(async () => {
|
|
await datenbankLeeren();
|
|
anmeldeRateLimit.zuruecksetzen();
|
|
process.env.APP_SECRET = GEHEIMNIS;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await testPrisma.$disconnect();
|
|
});
|
|
|
|
async function admin(passwort = "geheim1234") {
|
|
return testPrisma.benutzer.create({
|
|
data: {
|
|
email: "admin@example.de",
|
|
name: "Florian",
|
|
passwortHash: await hash(passwort),
|
|
},
|
|
});
|
|
}
|
|
|
|
it("meldet mit richtigem Passwort an und vermerkt den Zeitpunkt", async () => {
|
|
const benutzer = await admin();
|
|
const angemeldet = await mitPasswortAnmelden("admin@example.de", "geheim1234", "1.2.3.4");
|
|
|
|
expect(angemeldet.id).toBe(benutzer.id);
|
|
const danach = await testPrisma.benutzer.findUniqueOrThrow({ where: { id: benutzer.id } });
|
|
expect(danach.letzterLoginAm).not.toBeNull();
|
|
|
|
const protokoll = await testPrisma.auditEintrag.findFirst({ where: { aktion: "anmeldung.erfolgreich" } });
|
|
expect(protokoll).not.toBeNull();
|
|
});
|
|
|
|
it("lehnt falsches Passwort ab und protokolliert den Versuch", async () => {
|
|
await admin();
|
|
await expect(mitPasswortAnmelden("admin@example.de", "falsch", "1.2.3.4")).rejects.toThrow(AnmeldeFehler);
|
|
|
|
const protokoll = await testPrisma.auditEintrag.findFirst({ where: { aktion: "anmeldung.fehlgeschlagen" } });
|
|
expect(protokoll).not.toBeNull();
|
|
});
|
|
|
|
it("nennt bei unbekannter Adresse denselben Grund wie bei falschem Passwort", async () => {
|
|
await admin();
|
|
const unbekannt = await mitPasswortAnmelden("niemand@example.de", "x", "1.2.3.4").catch((f) => f);
|
|
const falsch = await mitPasswortAnmelden("admin@example.de", "falsch", "1.2.3.4").catch((f) => f);
|
|
|
|
expect(unbekannt.message).toBe(falsch.message);
|
|
});
|
|
|
|
it("lehnt deaktivierte Konten ab", async () => {
|
|
const benutzer = await admin();
|
|
await testPrisma.benutzer.update({ where: { id: benutzer.id }, data: { aktiv: false } });
|
|
|
|
await expect(mitPasswortAnmelden("admin@example.de", "geheim1234", "1.2.3.4")).rejects.toThrow(
|
|
AnmeldeFehler,
|
|
);
|
|
});
|
|
|
|
it("bremst nach fünf Fehlversuchen", async () => {
|
|
await admin();
|
|
for (let i = 0; i < 5; i++) {
|
|
await mitPasswortAnmelden("admin@example.de", "falsch", "9.9.9.9").catch(() => undefined);
|
|
}
|
|
|
|
await expect(mitPasswortAnmelden("admin@example.de", "geheim1234", "9.9.9.9")).rejects.toThrow(
|
|
/Zu viele Versuche/,
|
|
);
|
|
});
|
|
|
|
it("verknüpft eine OIDC-Anmeldung mit dem vorhandenen Konto", async () => {
|
|
const benutzer = await admin();
|
|
const angemeldet = await mitOidcAnmelden("subject-1", "admin@example.de", "Florian Wilde");
|
|
|
|
expect(angemeldet.id).toBe(benutzer.id);
|
|
expect(angemeldet.oidcSubject).toBe("subject-1");
|
|
expect(await testPrisma.benutzer.count()).toBe(1);
|
|
});
|
|
|
|
it("legt bei unbekannter OIDC-Anmeldung ein Konto an", async () => {
|
|
const angemeldet = await mitOidcAnmelden("subject-2", "Neu@example.de", "Neue Person");
|
|
|
|
expect(angemeldet.email).toBe("neu@example.de");
|
|
expect(angemeldet.passwortHash).toBeNull();
|
|
expect(await testPrisma.benutzer.count()).toBe(1);
|
|
});
|
|
|
|
it("erkennt dieselbe Subject-ID wieder, auch wenn sich die E-Mail ändert", async () => {
|
|
const erst = await mitOidcAnmelden("subject-3", "alt@example.de", "Person");
|
|
const wieder = await mitOidcAnmelden("subject-3", "neu@example.de", "Person");
|
|
|
|
expect(wieder.id).toBe(erst.id);
|
|
expect(await testPrisma.benutzer.count()).toBe(1);
|
|
});
|
|
});
|