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>
108 lines
3.9 KiB
JavaScript
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,
|
|
};
|