import { SignJWT, jwtVerify } from "jose"; /** * Das signierte Sitzungscookie der Web-GUI. * * Framework-frei, damit es ohne Next und ohne Datenbank testbar ist. Im Token steht nur, wer * angemeldet ist und wie. Rolle und Aktiv-Status stehen bewusst **nicht** darin – sie werden je * Anfrage aus der Datenbank gelesen, sonst wirkte ein Rechteentzug erst nach Ablauf der Sitzung. */ export const SITZUNG_COOKIE = "pd_sitzung"; /** Ein Arbeitstag. */ export const SITZUNG_LAUFZEIT_STUNDEN = 12; export type SitzungsHerkunft = "passwort" | "oidc"; export type Sitzung = { benutzerId: string; herkunft: SitzungsHerkunft; ausgestelltAm: number; }; const AUSSTELLER = "proxdeploy"; const ZIELGRUPPE = "proxdeploy-web"; const ALGORITHMUS = "HS256"; const MINDESTLAENGE = 32; export class SitzungsFehler extends Error { constructor(nachricht: string) { super(nachricht); this.name = "SitzungsFehler"; } } let zwischenspeicher: { quelle: string; schluessel: Uint8Array } | undefined; function schluesselHolen(): Uint8Array { const quelle = process.env["SITZUNG_GEHEIMNIS"] ?? ""; if (quelle.length < MINDESTLAENGE) { throw new SitzungsFehler( `SITZUNG_GEHEIMNIS fehlt oder ist zu kurz (mindestens ${MINDESTLAENGE} Zeichen). ` + "Erzeugen mit: openssl rand -base64 48", ); } if (zwischenspeicher?.quelle !== quelle) { zwischenspeicher = { quelle, schluessel: new TextEncoder().encode(quelle) }; } return zwischenspeicher.schluessel; } export async function sitzungErzeugen(daten: Omit): Promise { return await new SignJWT({ herkunft: daten.herkunft }) .setProtectedHeader({ alg: ALGORITHMUS }) .setSubject(daten.benutzerId) .setIssuer(AUSSTELLER) .setAudience(ZIELGRUPPE) .setIssuedAt() .setExpirationTime(`${SITZUNG_LAUFZEIT_STUNDEN}h`) .sign(schluesselHolen()); } /** Gibt undefined zurück, wenn das Token fehlt, abgelaufen, verfälscht oder fremd ist. */ export async function sitzungLesen(jwt: string | undefined): Promise { // Vor dem `try`: ein fehlendes Geheimnis ist ein Konfigurationsfehler, kein „nicht angemeldet“. const schluessel = schluesselHolen(); if (!jwt) return undefined; try { const { payload } = await jwtVerify(jwt, schluessel, { algorithms: [ALGORITHMUS], issuer: AUSSTELLER, audience: ZIELGRUPPE, }); const { sub, iat, herkunft } = payload; if ( typeof sub !== "string" || sub.length === 0 || typeof iat !== "number" || (herkunft !== "passwort" && herkunft !== "oidc") ) { return undefined; } return { benutzerId: sub, herkunft, ausgestelltAm: iat }; } catch { return undefined; } }