// @vitest-environment node // // Abweichend von der globalen jsdom-Umgebung (vitest.config.ts): "jose" prueft // seine Eingabe mit "instanceof Uint8Array", und das von jsdom bereitgestellte // TextEncoder liefert ein Uint8Array aus einem anderen JS-Realm — die Pruefung // schlaegt dann fehl, obwohl der Code stimmt. Im Betrieb laeuft dieses Modul // ohnehin nie in einem DOM. /** * Tests fuer die Token-Schicht der Anmeldung. * * Alle Token in session.ts sind mit DEMSELBEN Geheimnis signiert und tragen * denselben Aussteller und dieselbe Zielgruppe. Was sie unterscheidet, ist * allein das "type"-Feld. Damit ist die zentrale Frage nicht, ob ein gueltiger * Token angenommen wird, sondern ob ein Token FUER EINEN ANDEREN ZWECK * abgelehnt wird — sonst laesst sich der zweite Faktor umgehen, indem man das * Challenge-Cookie in das Session-Cookie kopiert. */ import { describe, test, expect } from "vitest"; // Vor dem Import setzen: getSecretKey wirft ohne diesen Wert. process.env.SESSION_SECRET = "test-secret-mit-mindestens-32-zeichen-laenge"; const { signSessionToken, verifySessionToken, signTwoFactorChallenge, verifyTwoFactorChallenge, signTwoFactorSetup, verifyTwoFactorSetup, signTwoFactorBackupCodesDisplay, verifyTwoFactorBackupCodesDisplay, signEntraOAuthState, verifyEntraOAuthState, } = await import("./session"); const CUSTOMER_ID = "customer_123"; describe("Session-Token", () => { test("gibt Kunde, Rolle und Sitzungsversion unveraendert zurueck", async () => { const token = await signSessionToken({ customerId: CUSTOMER_ID, role: "ADMIN", sessionVersion: 3, }); await expect(verifySessionToken(token)).resolves.toEqual({ customerId: CUSTOMER_ID, role: "ADMIN", sessionVersion: 3, }); }); test("stuft eine unbekannte Rolle auf KUNDE herunter statt sie zu uebernehmen", async () => { // Die Rolle im Token ist nur der schnelle Vorfilter fuer den Proxy; // autoritativ ist die Datenbank. Ein unerwarteter Wert darf trotzdem nie // als etwas anderes als die geringste Berechtigung durchgehen. const token = await signSessionToken({ customerId: CUSTOMER_ID, role: "SUPERADMIN" as never, sessionVersion: 0, }); await expect(verifySessionToken(token)).resolves.toMatchObject({ role: "KUNDE" }); }); test("lehnt einen manipulierten Token ab", async () => { const token = await signSessionToken({ customerId: CUSTOMER_ID, role: "KUNDE", sessionVersion: 0, }); // Letztes Zeichen der Signatur veraendern. const tampered = token.slice(0, -1) + (token.endsWith("A") ? "B" : "A"); await expect(verifySessionToken(tampered)).resolves.toBeNull(); }); test("lehnt Unsinn ab, statt zu werfen", async () => { await expect(verifySessionToken("kein.jwt.hier")).resolves.toBeNull(); await expect(verifySessionToken("")).resolves.toBeNull(); }); }); describe("Token-Verwechslung", () => { /** * Der wichtigste Test dieser Datei. * * Das Challenge-Cookie wird ausgestellt, NACHDEM das Passwort stimmt, aber * BEVOR der zweite Faktor geprueft ist. Gilt es als Session, ist die * Zwei-Faktor-Pruefung eine Formalie, die sich mit einem Kopiervorgang im * Browser ueberspringen laesst. */ test("ein 2FA-Challenge-Token ist KEINE gueltige Anmeldung", async () => { const challenge = await signTwoFactorChallenge(CUSTOMER_ID); await expect(verifySessionToken(challenge)).resolves.toBeNull(); }); test("ein 2FA-Einrichtungstoken ist KEINE gueltige Anmeldung", async () => { const setup = await signTwoFactorSetup(CUSTOMER_ID, "JBSWY3DPEHPK3PXP"); await expect(verifySessionToken(setup)).resolves.toBeNull(); }); test("ein Backup-Code-Anzeigetoken ist KEINE gueltige Anmeldung", async () => { const display = await signTwoFactorBackupCodesDisplay(CUSTOMER_ID, ["aaaa-bbbb"]); await expect(verifySessionToken(display)).resolves.toBeNull(); }); test("ein Entra-OAuth-State ist KEINE gueltige Anmeldung", async () => { const state = await signEntraOAuthState("zufallswert", "code-verifier"); await expect(verifySessionToken(state)).resolves.toBeNull(); }); test("ein Session-Token taugt umgekehrt fuer keinen der Sonderzwecke", async () => { const session = await signSessionToken({ customerId: CUSTOMER_ID, role: "ADMIN", sessionVersion: 0, }); await expect(verifyTwoFactorChallenge(session)).resolves.toBeNull(); await expect(verifyTwoFactorSetup(session)).resolves.toBeNull(); await expect(verifyTwoFactorBackupCodesDisplay(session)).resolves.toBeNull(); await expect(verifyEntraOAuthState(session)).resolves.toBeNull(); }); test("die Sondertoken lassen sich auch untereinander nicht tauschen", async () => { const challenge = await signTwoFactorChallenge(CUSTOMER_ID); const setup = await signTwoFactorSetup(CUSTOMER_ID, "JBSWY3DPEHPK3PXP"); // Ein Einrichtungstoken darf keinen Anmeldeschritt abschliessen, und ein // Challenge-Token traegt kein Secret, mit dem sich 2FA einrichten liesse. await expect(verifyTwoFactorChallenge(setup)).resolves.toBeNull(); await expect(verifyTwoFactorSetup(challenge)).resolves.toBeNull(); }); }); describe("Sondertoken im Normalfall", () => { test("Challenge und Einrichtung geben ihren Inhalt zurueck", async () => { const challenge = await signTwoFactorChallenge(CUSTOMER_ID); await expect(verifyTwoFactorChallenge(challenge)).resolves.toEqual({ customerId: CUSTOMER_ID, }); const setup = await signTwoFactorSetup(CUSTOMER_ID, "JBSWY3DPEHPK3PXP"); await expect(verifyTwoFactorSetup(setup)).resolves.toEqual({ customerId: CUSTOMER_ID, secret: "JBSWY3DPEHPK3PXP", }); }); test("der Entra-State haelt den PKCE-Verifier, der nie in die URL gehoert", async () => { const state = await signEntraOAuthState("zufallswert", "code-verifier"); await expect(verifyEntraOAuthState(state)).resolves.toEqual({ state: "zufallswert", codeVerifier: "code-verifier", }); }); });