375 lines
22 KiB
PHP
375 lines
22 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
// authentik-Anmeldung über OpenID Connect (Authorization-Code-Ablauf mit PKCE). Alle Funktionen beginnen mit authentik_.
|
|
|
|
const AUTHENTIK = 'authentik';
|
|
|
|
/** Einstellungen des Plugins (Geheimnis entschlüsselt). */
|
|
function authentik_config(): array
|
|
{
|
|
$secret = (string) plugin_setting(AUTHENTIK, 'client_secret_enc', '');
|
|
$label = trim((string) plugin_setting(AUTHENTIK, 'label', 'Mit authentik anmelden'));
|
|
return [
|
|
'enabled' => plugin_setting(AUTHENTIK, 'enabled', '0') === '1',
|
|
'issuer' => rtrim(trim((string) plugin_setting(AUTHENTIK, 'issuer', '')), '/'),
|
|
'client_id' => trim((string) plugin_setting(AUTHENTIK, 'client_id', '')),
|
|
'client_secret' => $secret !== '' ? decrypt_secret($secret) : '',
|
|
'scopes' => trim((string) plugin_setting(AUTHENTIK, 'scopes', 'openid email profile')) ?: 'openid email profile',
|
|
'group' => trim((string) plugin_setting(AUTHENTIK, 'group', '')),
|
|
'customers' => plugin_setting(AUTHENTIK, 'customers', '0') === '1',
|
|
'customer_create' => plugin_setting(AUTHENTIK, 'customer_create', '1') === '1',
|
|
'customer_group' => trim((string) plugin_setting(AUTHENTIK, 'customer_group', '')),
|
|
'require_verified' => plugin_setting(AUTHENTIK, 'require_verified', '1') === '1',
|
|
'label' => $label !== '' ? mb_substr($label, 0, 60) : 'Mit authentik anmelden',
|
|
];
|
|
}
|
|
|
|
/** Rückkehr-Adresse, die in authentik als „Redirect URI“ eingetragen wird. */
|
|
function authentik_redirect_uri(): string
|
|
{
|
|
return rtrim(base_url(), '/') . plugin_login_url(AUTHENTIK, 'callback');
|
|
}
|
|
|
|
/** Prüft eine Adresse des Anmeldedienstes: https (http nur für localhost zum Testen), ohne Zugangsdaten, Parameter und Anker. */
|
|
function authentik_url_ok(string $url, bool $allowQuery = false): bool
|
|
{
|
|
if ($url === '' || strlen($url) > 300 || preg_match('/[\x00-\x20]/', $url)) return false;
|
|
$p = parse_url($url);
|
|
if (!$p || empty($p['host']) || isset($p['user']) || isset($p['pass']) || isset($p['fragment'])) return false;
|
|
if (!$allowQuery && isset($p['query'])) return false;
|
|
$scheme = $p['scheme'] ?? '';
|
|
return $scheme === 'https' || ($scheme === 'http' && in_array($p['host'], ['localhost', '127.0.0.1', '[::1]'], true));
|
|
}
|
|
|
|
function authentik_ready(): bool
|
|
{
|
|
$c = authentik_config();
|
|
return $c['enabled'] && authentik_url_ok($c['issuer']) && $c['client_id'] !== '' && $c['client_secret'] !== '';
|
|
}
|
|
|
|
/** Ist die Anmeldung für Kunden (Ticketersteller) eingeschaltet und eingerichtet? */
|
|
function authentik_customer_ready(): bool
|
|
{
|
|
return authentik_ready() && authentik_config()['customers'];
|
|
}
|
|
|
|
/**
|
|
* HTTP-Aufruf zum Anmeldedienst mit cURL: Zertifikat wird geprüft, keine Weiterleitungen, begrenzte Antwortgröße und Zeit.
|
|
* @return array{0:int,1:?string,2:?string} [Status, Antworttext, Fehler]
|
|
*/
|
|
function authentik_http(string $method, string $url, array $opts = []): array
|
|
{
|
|
if (!function_exists('curl_init')) return [0, null, 'Auf diesem Server fehlt die PHP-Erweiterung cURL.'];
|
|
if (!authentik_url_ok($url, true)) return [0, null, 'Ungültige Adresse.'];
|
|
if (isset($GLOBALS['authentik_http']) && is_callable($GLOBALS['authentik_http'])) return ($GLOBALS['authentik_http'])($method, $url, $opts); // für Tests
|
|
$body = '';
|
|
$headers = array_merge(['Accept: application/json', 'User-Agent: DeskHub-authentik'], (array) ($opts['headers'] ?? []));
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 12, CURLOPT_CONNECTTIMEOUT => 6,
|
|
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
|
|
CURLOPT_WRITEFUNCTION => function ($c, string $data) use (&$body): int {
|
|
$body .= $data;
|
|
return strlen($body) > 262144 ? 0 : strlen($data);
|
|
},
|
|
]);
|
|
if ($method === 'POST') {
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query((array) ($opts['form'] ?? [])));
|
|
}
|
|
$ok = curl_exec($ch);
|
|
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
$err = $ok === false ? curl_error($ch) : null;
|
|
curl_close($ch);
|
|
return [$status, $err === null ? $body : null, $err];
|
|
}
|
|
|
|
/** Konfiguration des Anmeldedienstes (/.well-known/openid-configuration), eine Stunde lang gemerkt. @return array{ok:bool,error?:string,doc?:array} */
|
|
function authentik_discovery(string $issuer, bool $refresh = false): array
|
|
{
|
|
if (!$refresh) {
|
|
$cached = json_decode((string) plugin_setting(AUTHENTIK, 'discovery', ''), true);
|
|
if (is_array($cached) && ($cached['issuer'] ?? '') === $issuer && (int) ($cached['at'] ?? 0) > time() - 3600 && is_array($cached['doc'] ?? null)) return ['ok' => true, 'doc' => $cached['doc']];
|
|
}
|
|
[$status, $body, $err] = authentik_http('GET', $issuer . '/.well-known/openid-configuration');
|
|
if ($err !== null || $body === null) return ['ok' => false, 'error' => 'authentik ist nicht erreichbar: ' . ($err ?? 'keine Antwort')];
|
|
$doc = json_decode($body, true);
|
|
if ($status !== 200 || !is_array($doc)) return ['ok' => false, 'error' => 'authentik antwortet nicht mit einer gültigen Konfiguration (HTTP ' . $status . '). Stimmt die Issuer-Adresse?'];
|
|
if (rtrim((string) ($doc['issuer'] ?? ''), '/') !== $issuer) return ['ok' => false, 'error' => 'Der Issuer in der Antwort („' . mb_substr((string) ($doc['issuer'] ?? ''), 0, 120) . '“) passt nicht zur eingestellten Adresse.'];
|
|
foreach (['authorization_endpoint', 'token_endpoint'] as $k) {
|
|
if (!authentik_url_ok((string) ($doc[$k] ?? ''), true)) return ['ok' => false, 'error' => 'Die Konfiguration von authentik enthält keinen gültigen Eintrag „' . $k . '“.'];
|
|
}
|
|
$keep = array_intersect_key($doc, array_flip(['issuer', 'authorization_endpoint', 'token_endpoint', 'userinfo_endpoint']));
|
|
plugin_set_setting(AUTHENTIK, 'discovery', json_encode(['issuer' => $issuer, 'at' => time(), 'doc' => $keep], JSON_UNESCAPED_SLASHES));
|
|
return ['ok' => true, 'doc' => $keep];
|
|
}
|
|
|
|
function authentik_b64url(string $bin): string
|
|
{
|
|
return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
|
|
}
|
|
|
|
/** Ablauf starten: Zustand, Nonce und PKCE-Prüfwert in der Sitzung merken, dann weiter zu authentik. $mode: 'login' oder 'link' (Konto verknüpfen). */
|
|
function authentik_begin(string $mode, ?int $staffId = null): array
|
|
{
|
|
$back = $mode === 'link' ? ['back' => '/staff/plugin.php?p=authentik&page=konto'] : [];
|
|
if ($mode === 'customer' && !authentik_customer_ready()) return ['error' => 'Die Anmeldung mit authentik ist für Kunden nicht eingeschaltet.'];
|
|
if (!authentik_ready()) return ['error' => 'Die Anmeldung mit authentik ist nicht eingerichtet.'] + $back;
|
|
$cfg = authentik_config();
|
|
$d = authentik_discovery($cfg['issuer']);
|
|
if (!$d['ok']) return ['error' => $d['error']] + $back;
|
|
$verifier = authentik_b64url(random_bytes(48));
|
|
$state = bin2hex(random_bytes(16));
|
|
$nonce = bin2hex(random_bytes(16));
|
|
$_SESSION['authentik'] = ['state' => $state, 'nonce' => $nonce, 'verifier' => $verifier, 'at' => time(), 'mode' => $mode, 'staff_id' => $staffId];
|
|
$url = $d['doc']['authorization_endpoint'];
|
|
$url .= (str_contains($url, '?') ? '&' : '?') . http_build_query([
|
|
'response_type' => 'code', 'client_id' => $cfg['client_id'], 'redirect_uri' => authentik_redirect_uri(), 'scope' => $cfg['scopes'],
|
|
'state' => $state, 'nonce' => $nonce, 'code_challenge' => authentik_b64url(hash('sha256', $verifier, true)), 'code_challenge_method' => 'S256',
|
|
], '', '&', PHP_QUERY_RFC3986);
|
|
return ['redirect' => $url];
|
|
}
|
|
|
|
/** Start der Anmeldung (nicht angemeldet). */
|
|
function authentik_login_start(array $req): array
|
|
{
|
|
return authentik_begin('login');
|
|
}
|
|
|
|
/** Start der Anmeldung für Kunden (Kundenportal). */
|
|
function authentik_customer_start(array $req): array
|
|
{
|
|
return authentik_begin('customer');
|
|
}
|
|
|
|
/** Start der Verknüpfung (angemeldeter Mitarbeiter). */
|
|
function authentik_link_start(array $req): array
|
|
{
|
|
$staff = $req['staff'] ?? null;
|
|
if (!is_array($staff) || empty($staff['id'])) return ['error' => 'Bitte zuerst anmelden.'];
|
|
return authentik_begin('link', (int) $staff['id']);
|
|
}
|
|
|
|
/** Liest die Aussagen (Claims) eines ID-Tokens, das direkt vom Token-Endpunkt über TLS kam (dann ist keine Signaturprüfung nötig, OIDC Core 3.1.3.7). */
|
|
function authentik_claims(string $jwt): ?array
|
|
{
|
|
$parts = explode('.', $jwt);
|
|
if (count($parts) !== 3) return null;
|
|
$json = base64_decode(strtr($parts[1], '-_', '+/') . str_repeat('=', (4 - strlen($parts[1]) % 4) % 4), true);
|
|
$claims = $json === false ? null : json_decode($json, true);
|
|
return is_array($claims) ? $claims : null;
|
|
}
|
|
|
|
/**
|
|
* Rückkehr von authentik: Code gegen Token tauschen und alles prüfen (Zustand, Aussteller, Zielgruppe, Ablauf, Nonce, E-Mail, Gruppe).
|
|
* @return array{error:string}|array{claims:array,email:string,sub:string}
|
|
*/
|
|
function authentik_exchange(array $q, ?array $sess, string $mode): array
|
|
{
|
|
$cfg = authentik_config();
|
|
if (!$sess || time() - (int) ($sess['at'] ?? 0) > 600 || ($sess['mode'] ?? 'login') !== $mode) return ['error' => 'Die Anmeldung ist abgelaufen oder wurde nicht hier gestartet. Bitte noch einmal auf „' . ($mode === 'link' ? 'Mit authentik verknüpfen' : $cfg['label']) . '“ klicken.'];
|
|
if (isset($q['error'])) return ['error' => 'authentik hat die Anmeldung nicht bestätigt (' . mb_substr(preg_replace('/[^A-Za-z0-9 _.\-]/', '', (string) $q['error']), 0, 60) . ').'];
|
|
$state = (string) ($q['state'] ?? '');
|
|
$code = (string) ($q['code'] ?? '');
|
|
if ($state === '' || !hash_equals((string) $sess['state'], $state) || $code === '' || strlen($code) > 2000) return ['error' => 'Die Antwort von authentik passt nicht zur gestarteten Anmeldung. Bitte noch einmal versuchen.'];
|
|
|
|
$d = authentik_discovery($cfg['issuer']);
|
|
if (!$d['ok']) return ['error' => $d['error']];
|
|
[$status, $body, $err] = authentik_http('POST', $d['doc']['token_endpoint'], ['form' => [
|
|
'grant_type' => 'authorization_code', 'code' => $code, 'redirect_uri' => authentik_redirect_uri(),
|
|
'client_id' => $cfg['client_id'], 'client_secret' => $cfg['client_secret'], 'code_verifier' => (string) $sess['verifier'],
|
|
]]);
|
|
if ($err !== null || $body === null) return ['error' => 'authentik ist nicht erreichbar: ' . ($err ?? 'keine Antwort')];
|
|
$tok = json_decode($body, true);
|
|
if ($status !== 200 || !is_array($tok) || empty($tok['id_token'])) return ['error' => 'authentik hat den Anmelde-Code nicht angenommen (HTTP ' . $status . '). Stimmen Client-ID und Geheimnis?'];
|
|
$claims = authentik_claims((string) $tok['id_token']);
|
|
if (!$claims) return ['error' => 'Die Antwort von authentik ist unlesbar.'];
|
|
|
|
// Prüfung nach OIDC Core: Aussteller, Zielgruppe, Ablauf, Nonce
|
|
if (rtrim((string) ($claims['iss'] ?? ''), '/') !== $cfg['issuer']) return ['error' => 'Die Antwort stammt nicht vom eingestellten authentik (Aussteller).'];
|
|
$aud = $claims['aud'] ?? [];
|
|
$aud = is_array($aud) ? $aud : [$aud];
|
|
if (!in_array($cfg['client_id'], array_map('strval', $aud), true) || (isset($claims['azp']) && (string) $claims['azp'] !== $cfg['client_id'])) return ['error' => 'Die Antwort von authentik gilt für eine andere Anwendung.'];
|
|
if ((int) ($claims['exp'] ?? 0) < time() - 60) return ['error' => 'Die Bestätigung von authentik ist abgelaufen.'];
|
|
if (!hash_equals((string) $sess['nonce'], (string) ($claims['nonce'] ?? ''))) return ['error' => 'Die Antwort von authentik gehört nicht zu dieser Anmeldung (Nonce).'];
|
|
$sub = (string) ($claims['sub'] ?? '');
|
|
if ($sub === '' || strlen($sub) > 255) return ['error' => 'Die Antwort von authentik enthält keine Benutzerkennung.'];
|
|
|
|
$userinfo = null;
|
|
$loadUserinfo = function () use (&$userinfo, $tok, $d, $sub) {
|
|
if ($userinfo !== null) return $userinfo;
|
|
$userinfo = [];
|
|
if (!empty($tok['access_token']) && !empty($d['doc']['userinfo_endpoint'])) {
|
|
[$s2, $b2, $e2] = authentik_http('GET', $d['doc']['userinfo_endpoint'], ['headers' => ['Authorization: Bearer ' . $tok['access_token']]]);
|
|
$ui = $e2 === null && $b2 !== null && $s2 === 200 ? json_decode($b2, true) : null;
|
|
if (is_array($ui) && (string) ($ui['sub'] ?? '') === $sub) $userinfo = $ui; // nur der Benutzer dieser Anmeldung zählt
|
|
}
|
|
return $userinfo;
|
|
};
|
|
|
|
$email = strtolower(trim((string) ($claims['email'] ?? '')));
|
|
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
$claims = $loadUserinfo() + $claims; // manche Einstellungen liefern die Adresse nur über „userinfo“
|
|
$email = strtolower(trim((string) ($claims['email'] ?? '')));
|
|
}
|
|
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) return ['error' => 'authentik hat keine E-Mail-Adresse geliefert. Im Provider den Bereich „email“ aktivieren.'];
|
|
// Ob authentik die Adresse als bestätigt meldet. Verlangt wird das nur dort, wo die Adresse über das Konto entscheidet (siehe authentik_unverified_error()).
|
|
$verified = !array_key_exists('email_verified', $claims) || $claims['email_verified'] !== false;
|
|
|
|
// Optional: nur Mitglieder einer Gruppe (für Kunden eine eigene)
|
|
$needGroup = $mode === 'customer' ? $cfg['customer_group'] : $cfg['group'];
|
|
if ($needGroup !== '') {
|
|
$groups = $claims['groups'] ?? null;
|
|
if (!is_array($groups)) $groups = $loadUserinfo()['groups'] ?? null;
|
|
$names = array_map(fn($g) => is_array($g) ? (string) ($g['name'] ?? '') : (string) $g, is_array($groups) ? $groups : []);
|
|
if (!in_array($needGroup, $names, true)) {
|
|
audit_log(null, 'authentik: ' . mb_substr($email, 0, 120) . ' abgelehnt (nicht in der Gruppe ' . mb_substr($needGroup, 0, 60) . ')');
|
|
return ['error' => 'Ihr authentik-Konto gehört nicht zur Gruppe „' . $needGroup . '“, die für DeskHub nötig ist.'];
|
|
}
|
|
}
|
|
return ['claims' => $claims, 'email' => $email, 'sub' => $sub, 'verified' => $verified];
|
|
}
|
|
|
|
/**
|
|
* Fehlermeldung, wenn authentik die E-Mail-Adresse als „nicht bestätigt“ meldet und die Adresse über das Konto entscheiden würde
|
|
* (Mitarbeiter ohne Verknüpfung, Kunden). Null = in Ordnung (bestätigt oder Prüfung ausgeschaltet).
|
|
*/
|
|
function authentik_unverified_error(array $x, string $who): ?string
|
|
{
|
|
if ($x['verified'] || !authentik_config()['require_verified']) return null;
|
|
return $who === 'customer'
|
|
? 'Die E-Mail-Adresse ist in authentik nicht bestätigt. Bitte erst in authentik bestätigen.'
|
|
: 'Die E-Mail-Adresse ist in authentik nicht bestätigt, daher ordnet DeskHub das Konto nicht über die Adresse zu. Verknüpfen Sie Ihr Konto einmal unter „Mein authentik-Konto“ (nach der Anmeldung mit E-Mail und Passwort), danach geht die Anmeldung mit authentik.';
|
|
}
|
|
|
|
// ── Verknüpfung DeskHub-Konto ⇄ authentik-Konto (in den Einstellungen gespeichert) ──
|
|
|
|
/** Kurzer Schlüssel für eine authentik-Benutzerkennung (gilt nur für diesen Issuer). */
|
|
function authentik_link_hash(string $sub): string
|
|
{
|
|
return substr(hash('sha256', authentik_config()['issuer'] . '|' . $sub), 0, 40);
|
|
}
|
|
|
|
/** Zu dieser authentik-Kennung verknüpfter Mitarbeiter (ID) oder null. */
|
|
function authentik_link_staff_for(string $sub): ?int
|
|
{
|
|
$id = (int) plugin_setting(AUTHENTIK, 'link_s_' . authentik_link_hash($sub), '0');
|
|
return $id > 0 ? $id : null;
|
|
}
|
|
|
|
/** Verknüpfung eines Mitarbeiters: ['h' => Schlüssel, 'email' => …, 'at' => …] oder null. */
|
|
function authentik_link_of(int $staffId): ?array
|
|
{
|
|
$d = json_decode((string) plugin_setting(AUTHENTIK, 'link_u_' . $staffId, ''), true);
|
|
return is_array($d) && !empty($d['h']) ? $d : null;
|
|
}
|
|
|
|
function authentik_link_set(int $staffId, string $sub, string $email): void
|
|
{
|
|
authentik_link_remove($staffId);
|
|
$h = authentik_link_hash($sub);
|
|
plugin_set_setting(AUTHENTIK, 'link_s_' . $h, (string) $staffId);
|
|
plugin_set_setting(AUTHENTIK, 'link_u_' . $staffId, json_encode(['h' => $h, 'email' => mb_substr($email, 0, 190), 'at' => date('Y-m-d H:i:s')], JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
function authentik_link_remove(int $staffId): void
|
|
{
|
|
$old = authentik_link_of($staffId);
|
|
$keys = ['link_u_' . $staffId];
|
|
if ($old) $keys[] = 'link_s_' . $old['h'];
|
|
foreach ($keys as $k) {
|
|
plugin_set_setting(AUTHENTIK, $k, ''); // erst leeren (der Einstellungs-Zwischenspeicher der Anfrage), dann die Zeile löschen
|
|
get_db()->prepare('DELETE FROM settings WHERE setting_key = ?')->execute(['plugin.' . AUTHENTIK . '.' . $k]);
|
|
}
|
|
}
|
|
|
|
/** Alle Verknüpfungen mit Name und E-Mail des Mitarbeiters (für die Verwaltung). @return list<array{staff_id:int,name:string,staff_email:string,email:string,at:string}> */
|
|
function authentik_links_all(): array
|
|
{
|
|
$out = [];
|
|
$rows = get_db()->query("SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'plugin.authentik.link_u_%'")->fetchAll();
|
|
$st = get_db()->prepare('SELECT name, email FROM staff WHERE id = ?');
|
|
foreach ($rows as $r) {
|
|
if (!str_starts_with((string) $r['setting_key'], 'plugin.authentik.link_u_')) continue;
|
|
$id = (int) substr((string) $r['setting_key'], strlen('plugin.authentik.link_u_'));
|
|
$d = json_decode((string) $r['setting_value'], true);
|
|
if ($id <= 0 || !is_array($d)) continue;
|
|
$st->execute([$id]);
|
|
$s = $st->fetch();
|
|
$out[] = ['staff_id' => $id, 'name' => (string) ($s['name'] ?? '(gelöscht)'), 'staff_email' => (string) ($s['email'] ?? ''), 'email' => (string) ($d['email'] ?? ''), 'at' => (string) ($d['at'] ?? '')];
|
|
}
|
|
usort($out, fn($a, $b) => strcasecmp($a['name'], $b['name']));
|
|
return $out;
|
|
}
|
|
|
|
/** Rückkehr von authentik: angemeldet = Konto verknüpfen, sonst anmelden. */
|
|
function authentik_login_callback(array $req): array
|
|
{
|
|
$loggedIn = isset($req['staff']) && is_array($req['staff']);
|
|
$sess = is_array($_SESSION['authentik'] ?? null) ? $_SESSION['authentik'] : null;
|
|
unset($_SESSION['authentik']); // jeder Ablauf gilt nur einmal
|
|
// Betriebsart: angemeldet = Verknüpfen; sonst steht in der Sitzung, ob ein Kunde oder ein Mitarbeiter anmelden wollte
|
|
$mode = $loggedIn ? 'link' : (($sess['mode'] ?? 'login') === 'customer' ? 'customer' : 'login');
|
|
$back = $loggedIn ? ['back' => '/staff/plugin.php?p=authentik&page=konto'] : ($mode === 'customer' ? ['audience' => 'customer'] : []);
|
|
if (!authentik_ready()) return ['error' => 'Die Anmeldung mit authentik ist nicht eingerichtet.'] + $back;
|
|
if ($mode === 'customer' && !authentik_customer_ready()) return ['error' => 'Die Anmeldung mit authentik ist für Kunden nicht eingeschaltet.'] + $back;
|
|
$x = authentik_exchange((array) ($req['query'] ?? []), $sess, $mode);
|
|
if (isset($x['error'])) return $x + $back;
|
|
$email = $x['email'];
|
|
$sub = $x['sub'];
|
|
|
|
// Kunde (Ticketersteller): nur die bestätigte E-Mail-Adresse zählt; das Konto wird bei Bedarf angelegt
|
|
if ($mode === 'customer') {
|
|
if ($e = authentik_unverified_error($x, 'customer')) return ['error' => $e] + $back;
|
|
$c = $x['claims'];
|
|
$name = trim((string) ($c['name'] ?? ''));
|
|
if ($name === '') $name = trim(trim((string) ($c['given_name'] ?? '')) . ' ' . trim((string) ($c['family_name'] ?? '')));
|
|
if ($name === '') $name = trim((string) ($c['preferred_username'] ?? ''));
|
|
return ['customer' => ['email' => $email, 'name' => mb_substr($name, 0, 120), 'create' => authentik_config()['customer_create'], 'via' => 'authentik']];
|
|
}
|
|
|
|
// Verknüpfen (angemeldeter Mitarbeiter)
|
|
if ($loggedIn) {
|
|
$staffId = (int) $req['staff']['id'];
|
|
if ((int) ($sess['staff_id'] ?? 0) !== $staffId) return ['error' => 'Die Verknüpfung wurde von einem anderen Konto gestartet. Bitte noch einmal starten.'] + $back;
|
|
$other = authentik_link_staff_for($sub);
|
|
if ($other !== null && $other !== $staffId) return ['error' => 'Dieses authentik-Konto ist bereits mit einem anderen DeskHub-Konto verknüpft.'] + $back;
|
|
authentik_link_set($staffId, $sub, $email);
|
|
audit_log($staffId, 'authentik: Konto verknüpft (' . mb_substr($email, 0, 120) . ')');
|
|
return ['local' => '/staff/plugin.php?p=authentik&page=konto', 'notice' => 'Ihr authentik-Konto (' . $email . ') ist jetzt verknüpft. Die Anmeldung mit authentik führt künftig genau in dieses Konto.'];
|
|
}
|
|
|
|
// Anmelden: zuerst über die Verknüpfung, sonst (wenn erlaubt) über die E-Mail-Adresse
|
|
$st = get_db()->prepare('SELECT id, is_active FROM staff WHERE id = ?');
|
|
$linked = authentik_link_staff_for($sub);
|
|
if ($linked !== null) {
|
|
$st->execute([$linked]);
|
|
$s = $st->fetch();
|
|
if (!$s) { authentik_link_remove($linked); }
|
|
elseif (!(int) $s['is_active']) return ['error' => 'Das mit Ihrem authentik-Konto verknüpfte DeskHub-Konto ist deaktiviert.'];
|
|
else return ['staff_id' => (int) $s['id'], 'via' => 'authentik'];
|
|
}
|
|
if (plugin_setting(AUTHENTIK, 'only_linked', '0') === '1') {
|
|
audit_log(null, 'authentik: ' . mb_substr($email, 0, 120) . ' abgelehnt (nicht verknüpft)');
|
|
return ['error' => 'Ihr authentik-Konto ist noch keinem DeskHub-Konto zugeordnet. Melden Sie sich mit E-Mail und Passwort an und verknüpfen Sie es dort unter „Mein authentik-Konto“.'];
|
|
}
|
|
// Zuordnung über die E-Mail-Adresse nur, wenn authentik sie als bestätigt meldet (sonst über „Mein authentik-Konto“ verknüpfen)
|
|
if ($e = authentik_unverified_error($x, 'staff')) return ['error' => $e];
|
|
$q = get_db()->prepare('SELECT id FROM staff WHERE LOWER(email) = ? AND is_active = 1');
|
|
$q->execute([$email]);
|
|
$id = $q->fetchColumn();
|
|
if (!$id) {
|
|
audit_log(null, 'authentik: kein Mitarbeiterkonto für ' . mb_substr($email, 0, 120));
|
|
return ['error' => 'Für ' . $email . ' gibt es in DeskHub kein aktives Mitarbeiterkonto. Bitte einen Administrator bitten, es mit derselben E-Mail-Adresse anzulegen, oder ein bestehendes Konto unter „Mein authentik-Konto“ verknüpfen.'];
|
|
}
|
|
$id = (int) $id;
|
|
// Hat dieses DeskHub-Konto schon ein anderes authentik-Konto, darf ein fremdes mit gleicher E-Mail-Adresse nicht hinein
|
|
if (authentik_link_of($id) !== null) return ['error' => 'Dieses DeskHub-Konto ist mit einem anderen authentik-Konto verknüpft.'];
|
|
// Erste Anmeldung über die E-Mail-Adresse: gleich fest verknüpfen (spätere Änderungen der Adresse stören dann nicht)
|
|
authentik_link_set($id, $sub, $email);
|
|
audit_log($id, 'authentik: Konto automatisch verknüpft (' . mb_substr($email, 0, 120) . ')');
|
|
return ['staff_id' => $id, 'via' => 'authentik'];
|
|
}
|