import { scryptSync } from "node:crypto"; import { SignJWT, jwtVerify } from "jose"; /** * Kurzlebige, signierte Nutzlast fuer Zustaende, die zwischen zwei Anfragen * ueberleben muessen (#007) – heute der OIDC-Anmeldefluss mit `state`, `nonce`, * PKCE-Pruefwert und dem urspruenglich angeforderten Ziel. * * Bewusst getrennt von der Sitzung in src/lib/sitzung.ts: das hier lebt Minuten * statt Stunden, ist einmalig und traegt keine Identitaet. Beide sind ueber * `audience` gegeneinander abgegrenzt, ein Token kann also nicht an der falschen * Stelle durchrutschen. */ const ALGORITHMUS = "HS256"; const HERAUSGEBER = "laufzettel"; const SALZ = "laufzettel.kurzzeit.v1"; const MINDESTLAENGE = 32; export class KurzzeitFehler extends Error { constructor(nachricht: string) { super(nachricht); this.name = "KurzzeitFehler"; } } /** * Der Schluessel wird erst beim ersten Aufruf abgeleitet, damit `next build` ohne * gesetztes APP_SECRET laeuft. Die Quelle wird mitgemerkt, damit ein geaendertes * Geheimnis nicht am Zwischenspeicher vorbeilaeuft. */ let abgeleitet: { quelle: string; schluessel: Uint8Array } | undefined; function schluesselHolen(): Uint8Array { const quelle = process.env["APP_SECRET"] ?? ""; if (quelle.length < MINDESTLAENGE) { throw new KurzzeitFehler( `APP_SECRET fehlt oder ist zu kurz (mindestens ${MINDESTLAENGE} Zeichen). ` + "Erzeugen mit: openssl rand -base64 48", ); } if (abgeleitet?.quelle !== quelle) { abgeleitet = { quelle, schluessel: new Uint8Array(scryptSync(quelle, SALZ, 32)) }; } return abgeleitet.schluessel; } /** Signiert eine Nutzlast fuer den angegebenen Zweck und die angegebene Dauer. */ export async function kurzzeitTokenErzeugen( zweck: string, nutzlast: Record, gueltigkeitSekunden: number, ): Promise { return new SignJWT(nutzlast) .setProtectedHeader({ alg: ALGORITHMUS }) .setIssuedAt() .setIssuer(HERAUSGEBER) .setAudience(zweck) .setExpirationTime(`${gueltigkeitSekunden}s`) .sign(schluesselHolen()); } /** * Liest eine Nutzlast zurueck. Gibt `undefined` zurueck, wenn das Token fehlt, * abgelaufen, verfaelscht oder fuer einen anderen Zweck ausgestellt ist – der * Aufrufer behandelt all das gleich. */ export async function kurzzeitTokenLesen( zweck: string, token: string | undefined, ): Promise | undefined> { if (!token) return undefined; try { const { payload } = await jwtVerify(token, schluesselHolen(), { algorithms: [ALGORITHMUS], issuer: HERAUSGEBER, audience: zweck, }); const werte: Record = {}; for (const [schluessel, wert] of Object.entries(payload)) { if (typeof wert === "string") werte[schluessel] = wert; } return werte; } catch (fehler) { // Ein Konfigurationsfehler ist etwas anderes als ein ungueltiges Token. if (fehler instanceof KurzzeitFehler) throw fehler; return undefined; } }