Files
timer-app/backend/oidc.js
T
adminandClaude Opus 5 a7e77793a8 Timer-App unter Versionsverwaltung stellen
Stand der laufenden Anwendung von 192.168.1.5:/opt/timer-app (systemd-Dienst
timer-app, Port 3003). Zeiterfassung mit Kostenberechnung: Node/Express,
SQLite, Anmeldung ausschliesslich ueber Authentik (OIDC, PKCE, RP-Logout) -
der frueher vorhandene Passwort-Login ist entfernt.

Nicht im Repo, bewusst:
- backend/.env und die .env-Sicherung: enthalten Session-Secret und die
  Authentik-Zugangsdaten. Vorlage ist backend/.env.example.
- data/: die Datenbank mit den echten Zeiterfassungen.
- node_modules/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 17:47:07 +02:00

108 lines
3.9 KiB
JavaScript

// ---------------------------------------------------------------------------
// 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,
};