From ebfa27d5c31ded302159af8b66f1f104c02525d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 6 Jun 2026 05:47:20 +0000 Subject: [PATCH] Generisches OIDC-SSO (weitere Auth-Quelle) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - oidc_callback.php (Auth-Code-Flow, state-Validierung, Code->Token->Userinfo) - login.php: OIDC-Button (konfigurierbarer Name) - Settings in admin/integrations.php (Endpoints, Client, Scopes) - nutzt vorhandene externe-SSO-Verknüpfung (loginWithMicrosoft, sub als ID) --- admin/integrations.php | 31 +++++++++++++ login.php | 28 ++++++++++++ oidc_callback.php | 101 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 oidc_callback.php diff --git a/admin/integrations.php b/admin/integrations.php index 23dde39..2da737f 100644 --- a/admin/integrations.php +++ b/admin/integrations.php @@ -32,6 +32,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save'])) { $db->setSetting('twilio_sid', trim($_POST['twilio_sid'] ?? '')); $db->setSetting('twilio_from', trim($_POST['twilio_from'] ?? '')); if (!empty($_POST['twilio_token'])) { $db->setSetting('twilio_token', trim($_POST['twilio_token'])); } + $db->setSetting('oidc_enabled', isset($_POST['oidc_enabled']) ? '1' : '0'); + $db->setSetting('oidc_name', trim($_POST['oidc_name'] ?? 'SSO')); + $db->setSetting('oidc_client_id', trim($_POST['oidc_client_id'] ?? '')); + $db->setSetting('oidc_auth_url', trim($_POST['oidc_auth_url'] ?? '')); + $db->setSetting('oidc_token_url', trim($_POST['oidc_token_url'] ?? '')); + $db->setSetting('oidc_userinfo_url', trim($_POST['oidc_userinfo_url'] ?? '')); + $db->setSetting('oidc_scopes', trim($_POST['oidc_scopes'] ?? 'openid profile email')); + if (!empty($_POST['oidc_client_secret'])) { $db->setSetting('oidc_client_secret', trim($_POST['oidc_client_secret'])); } $db->setSetting('user_daily_voucher_limit', max(0, (int)($_POST['user_daily_voucher_limit'] ?? 0))); $db->setSetting('trusted_proxy', trim($_POST['trusted_proxy'] ?? '')); $db->setSetting('webhook_enabled', isset($_POST['webhook_enabled']) ? '1' : '0'); @@ -57,6 +65,14 @@ $smsEnabled = $db->getSetting('sms_enabled', '0') === '1'; $twilioSid = $db->getSetting('twilio_sid', ''); $twilioFrom = $db->getSetting('twilio_from', ''); $twilioTokenSet = $db->getSetting('twilio_token', '') !== ''; +$oidcEnabled = $db->getSetting('oidc_enabled', '0') === '1'; +$oidcName = $db->getSetting('oidc_name', 'SSO'); +$oidcClientId = $db->getSetting('oidc_client_id', ''); +$oidcAuthUrl = $db->getSetting('oidc_auth_url', ''); +$oidcTokenUrl = $db->getSetting('oidc_token_url', ''); +$oidcUserinfoUrl = $db->getSetting('oidc_userinfo_url', ''); +$oidcScopes = $db->getSetting('oidc_scopes', 'openid profile email'); +$oidcSecretSet = $db->getSetting('oidc_client_secret', '') !== ''; $dailyLimit = (int)$db->getSetting('user_daily_voucher_limit', 0); $trustedProxy = $db->getSetting('trusted_proxy', ''); $webhookEnabled = $db->getSetting('webhook_enabled', '0') === '1'; @@ -145,6 +161,21 @@ label { display:block; font-size:14px; color:var(--text-secondary); margin:14px +
+

Single Sign-On (OpenID Connect)

+

Generischer OIDC-Provider (z.B. Keycloak, Authentik, Google, Auth0). Redirect-URI:

+ +
+
+
+
+
+ + + + +
+

Datenhaltung & Cleanup (DSGVO)

Aufbewahrungsfristen in Tagen (0 = deaktiviert). Ausführung per cron_cleanup.php (täglich empfohlen). diff --git a/login.php b/login.php index 341b1b6..1530eef 100644 --- a/login.php +++ b/login.php @@ -94,6 +94,27 @@ try { $m365LoginUrl = "https://login.microsoftonline.com/$m365TenantId/oauth2/v2.0/authorize?" . http_build_query($params); } + // Generisches OIDC (optional) + $oidcEnabled = $db->getSetting('oidc_enabled', '0') === '1' + && $db->getSetting('oidc_client_id', '') !== '' + && $db->getSetting('oidc_auth_url', '') !== ''; + $oidcName = $db->getSetting('oidc_name', 'SSO'); + $oidcLoginUrl = ''; + if ($oidcEnabled) { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; + $scriptPath = dirname($_SERVER['SCRIPT_NAME']); + $scriptPath = $scriptPath === '/' ? '' : $scriptPath; + $oidcState = bin2hex(random_bytes(16)); + $_SESSION['oidc_state'] = $oidcState; + $oidcLoginUrl = rtrim($db->getSetting('oidc_auth_url', ''), '?') . '?' . http_build_query([ + 'client_id' => $db->getSetting('oidc_client_id', ''), + 'response_type' => 'code', + 'redirect_uri' => $protocol . '://' . $_SERVER['HTTP_HOST'] . $scriptPath . '/oidc_callback.php', + 'scope' => $db->getSetting('oidc_scopes', 'openid profile email'), + 'state' => $oidcState, + ]); + } + $showLocalLogin = isset($_GET['local']) && $_GET['local'] === '1'; } catch (Exception $e) { @@ -223,6 +244,13 @@ try { + +

+ + 🔑 + + + diff --git a/oidc_callback.php b/oidc_callback.php new file mode 100644 index 0000000..852889c --- /dev/null +++ b/oidc_callback.php @@ -0,0 +1,101 @@ +getSetting('oidc_client_id', ''); +$clientSecret = $db->getSetting('oidc_client_secret', ''); +$tokenUrl = $db->getSetting('oidc_token_url', ''); +$userinfoUrl = $db->getSetting('oidc_userinfo_url', ''); + +if ($db->getSetting('oidc_enabled', '0') !== '1' || $clientId === '' || $tokenUrl === '' || $userinfoUrl === '') { + die('OIDC ist nicht konfiguriert. Zurück zum Login'); +} + +if (isset($_GET['error'])) { + die('OIDC-Fehler: ' . htmlspecialchars($_GET['error']) . '
Zurück zum Login'); +} +if (!isset($_GET['code'])) { + die('Kein Authorization Code erhalten.
Zurück zum Login'); +} + +// State validieren (CSRF) +$sessionState = $_SESSION['oidc_state'] ?? ''; +$returnedState = $_GET['state'] ?? ''; +unset($_SESSION['oidc_state']); +if ($sessionState === '' || !hash_equals($sessionState, $returnedState)) { + die('Ungültiger Sicherheits-Token (state).
Zurück zum Login'); +} + +$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; +$scriptPath = dirname($_SERVER['SCRIPT_NAME']); +$scriptPath = $scriptPath === '/' ? '' : $scriptPath; +$redirectUri = $protocol . '://' . $_SERVER['HTTP_HOST'] . $scriptPath . '/oidc_callback.php'; + +// Code -> Token +$ch = curl_init($tokenUrl); +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query([ + 'grant_type' => 'authorization_code', + 'code' => $_GET['code'], + 'redirect_uri' => $redirectUri, + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + ]), + CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, +]); +$resp = curl_exec($ch); +$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); +$token = json_decode((string)$resp, true); + +if ($httpCode !== 200 || empty($token['access_token'])) { + die('Token-Abruf fehlgeschlagen (HTTP ' . $httpCode . ').
Zurück zum Login'); +} + +// Userinfo abrufen +$ch = curl_init($userinfoUrl); +curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token['access_token'], 'Accept: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, +]); +$uResp = curl_exec($ch); +$uCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); +$info = json_decode((string)$uResp, true); + +if ($uCode !== 200 || !is_array($info)) { + die('Benutzerinfo-Abruf fehlgeschlagen (HTTP ' . $uCode . ').
Zurück zum Login'); +} + +$sub = $info['sub'] ?? ($info['id'] ?? ''); +$email = $info['email'] ?? ($info['preferred_username'] ?? ''); +$name = $info['name'] ?? trim(($info['given_name'] ?? '') . ' ' . ($info['family_name'] ?? '')); +if ($sub === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + die('OIDC lieferte keine gültige Identität (sub/email).
Zurück zum Login'); +} + +try { + // Wiederverwendung der externen-SSO-Verknüpfung (microsoft_id = externe ID) + $auth->loginWithMicrosoft(['id' => 'oidc:' . $sub, 'email' => $email, 'name' => $name ?: $email]); + header('Location: index.php'); + exit; +} catch (Exception $e) { + die('Login-Fehler: ' . htmlspecialchars($e->getMessage()) . '
Zurück zum Login'); +}