// --------------------------------------------------------------------------- // Authentik-Anbindung (OpenID Connect) — gleiches Muster wie das VVL-Tool. // // openid-client v6 ist ESM-only, diese App ist CommonJS. Deshalb wird das // Modul einmalig per dynamischem import() geladen und zwischengespeichert. // Ist OIDC nicht konfiguriert (Variablen fehlen), bleibt der Passwort-Login // aktiv und diese Funktionen melden "nicht bereit". // --------------------------------------------------------------------------- const ISSUER = process.env.OIDC_ISSUER || ''; const CLIENT_ID = process.env.OIDC_CLIENT_ID || ''; const CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || ''; const REDIRECT_URI = process.env.OIDC_REDIRECT_URI || ''; const ALLOW_INSECURE = String(process.env.OIDC_ALLOW_INSECURE || '') === 'true'; const SCOPE = 'openid profile email'; let oidc = null; // das openid-client-Modul (via dynamic import) let oidcConfig = null; // Ergebnis der Discovery function isConfigured() { return Boolean(ISSUER && CLIENT_ID && CLIENT_SECRET && REDIRECT_URI); } function isReady() { return oidcConfig !== null; } /** Discovery beim Serverstart. Wirft nicht — der Server startet auch, wenn * Authentik gerade nicht erreichbar ist (Login schlaegt dann fehl). */ async function init() { if (!isConfigured()) { console.warn('[oidc] Authentik nicht konfiguriert (OIDC_ISSUER/CLIENT_ID/CLIENT_SECRET/REDIRECT_URI) — nur Passwort-Login aktiv.'); return; } try { oidc = await import('openid-client'); oidcConfig = await oidc.discovery( new URL(ISSUER), CLIENT_ID, CLIENT_SECRET, undefined, ALLOW_INSECURE ? { execute: [oidc.allowInsecureRequests] } : undefined ); console.log('[oidc] Discovery erfolgreich:', ISSUER); } catch (err) { console.error('[oidc] Discovery fehlgeschlagen:', err.message || err); } } /** Baut die Weiterleitungs-URL zu Authentik (PKCE). */ async function buildLoginUrl() { if (!oidcConfig) throw new Error('OIDC nicht initialisiert'); const codeVerifier = oidc.randomPKCECodeVerifier(); const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier); const state = oidc.randomState(); const url = oidc.buildAuthorizationUrl(oidcConfig, { redirect_uri: REDIRECT_URI, scope: SCOPE, code_challenge: codeChallenge, code_challenge_method: 'S256', state, }); return { url: url.href, codeVerifier, state }; } /** Tauscht den Code gegen Tokens und liefert die Nutzer-Claims. */ async function handleCallback(currentUrl, codeVerifier, state) { if (!oidcConfig) throw new Error('OIDC nicht initialisiert'); const tokens = await oidc.authorizationCodeGrant(oidcConfig, currentUrl, { pkceCodeVerifier: codeVerifier, expectedState: state, }); const claims = tokens.claims(); if (!claims || !claims.sub) throw new Error('ID-Token ohne sub-Claim'); return { user: { sub: claims.sub, username: claims.preferred_username || claims.sub, displayName: claims.name || claims.preferred_username || null, email: claims.email || null, }, idToken: tokens.id_token || '', }; } /** RP-Initiated Logout: beendet auch die Authentik-Sitzung. Liefert null, * wenn kein end_session_endpoint vorhanden oder OIDC nicht bereit ist. */ function buildLogoutUrl(idToken) { if (!oidcConfig) return null; try { const params = { post_logout_redirect_uri: new URL(REDIRECT_URI).origin + '/' }; if (idToken) params.id_token_hint = idToken; return oidc.buildEndSessionUrl(oidcConfig, params).href; } catch { return null; } } module.exports = { init, isReady, isConfigured, buildLoginUrl, handleCallback, buildLogoutUrl, REDIRECT_URI, };