import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"; import type { NextAuthConfig } from "next-auth"; // Der Teil der Anmeldung, der **ohne Datenbank** auskommt. // // Das ist keine Stilfrage: proxy.ts läuft je nach Betriebsart in einer // Umgebung ohne Node-Module — dort gibt es kein `pg` und keine Verbindung. // Würde der Proxy die vollständige Konfiguration laden, zöge er die // Zugriffsschicht mit hinein und liesse sich nicht mehr ausliefern. Deshalb // hier nur Anbieter und Sitzungsregeln; alles, was die Datenbank berührt, // steht in auth.ts. /** Wie lange eine Anmeldung ohne erneuten Besuch bei Entra gilt. */ const SESSION_MAX_AGE_SECONDS = 60 * 60 * 9; // ein Arbeitstag /** * Der Aussteller — mit Abbruch statt Rückfall. * * Ohne diese Prüfung setzt Auth.js bei fehlender Variablen stillschweigend * `https://login.microsoftonline.com/common/v2.0` ein. Das ist beim ersten * Ausprobieren aufgefallen: die Weiterleitung ging tatsächlich nach * `/common/oauth2/v2.0/authorize`, und damit hätte sich **jedes** * Microsoft-Konto anmelden dürfen, auch ein privates. * * Aufgefallen wäre das im Betrieb sonst nicht — die Anmeldung funktioniert * ja, nur eben für zu viele. Ein vergessener Wert in der Deployment-Umgebung * muss deshalb den Start verhindern, nicht die Tür aufmachen. * * In der Entwicklung bleibt es bei einer Warnung: dort ist nichts * konfiguriert, und ein Abbruch beim Laden des Moduls nähme auch die * Anmeldeseite mit. */ function tenantIssuer(): string | undefined { const issuer = process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER; if (issuer) return issuer; const hinweis = "AUTH_MICROSOFT_ENTRA_ID_ISSUER fehlt. Ohne Mandanten-Aussteller fiele die " + "Anmeldung auf /common/ zurück und stünde jedem Microsoft-Konto offen."; if (process.env.NODE_ENV === "production") throw new Error(hinweis); console.warn(`[auth] ${hinweis}`); return undefined; } /** * Die Konfiguration als **Funktion**, nicht als Objekt. * * Auth.js wertet die Funktionsform pro Anfrage aus. Das ist hier nötig, weil * tenantIssuer() in der Produktion abbricht: als Objekt gebaut liefe die * Prüfung schon beim Import — und `next build` importiert jedes Route-Modul, * um die Seitendaten einzusammeln. Der Bau bräuchte dann die * Anmeldekonfiguration der Zielumgebung, und ein Abbild liesse sich nicht * mehr einmal bauen und überall ausliefern. */ export function authConfig(): NextAuthConfig { return { providers: [ MicrosoftEntraID({ clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID, clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET, issuer: tenantIssuer(), // Der eingebaute Anbieter fordert zusätzlich `User.Read` an und holt // damit das Profilbild aus dem Graph. Beides ist hier unerwünscht: eine // Berechtigung, die niemand braucht, muss die Mandantenverwaltung // trotzdem genehmigen — und das Bild landete base64-kodiert im // Sitzungscookie, das dann in Teile zerfällt. authorization: { params: { scope: "openid profile email" } }, profile(profile) { return { // Die `oid`, nicht `sub`: `sub` ist pro Anwendung verschieden, die // `oid` identifiziert dieselbe Person über Anwendungen hinweg und // überlebt Namens- und Adressänderungen. id: profile.oid, name: profile.name ?? null, // `email` ist im Token ein optionaler Anspruch — je nach Mandant // fehlt er. `preferred_username` bzw. `upn` tragen dann dieselbe // Adresse. Ohne diesen Rückfall scheitert die Anmeldung in genau // den Mandanten, die den Anspruch nicht ausdrücklich konfiguriert // haben. email: profile.email ?? profile.preferred_username ?? profile.upn ?? null, image: null, }; }, }), ], // Eigene Seite statt der von Auth.js mitgelieferten: die Anmeldung ist die // erste Seite, die jemand sieht, und soll aussehen wie die Anwendung. pages: { signIn: "/login", error: "/login" }, session: { strategy: "jwt", maxAge: SESSION_MAX_AGE_SECONDS }, // Hinter Reverse Proxy und Container-Netzwerk kommt der Host aus dem // Header. Ohne das verweigert Auth.js in der Produktion den Dienst, weil // es die Herkunft nicht bestätigen kann. trustHost: true, }; }