Upload via GUI (49 Dateien)
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# authentik-Anmeldung für DeskHub
|
||||
|
||||
Mitarbeiter und Kunden melden sich mit **authentik** an (OpenID Connect, Authorization-Code-Ablauf mit PKCE). Auf der Anmeldeseite des Mitarbeiterbereichs und, wenn eingeschaltet, des Kundenportals erscheint zusätzlich der Knopf „Mit authentik anmelden“. E-Mail und Passwort bleiben als Zugang erhalten.
|
||||
|
||||
Voraussetzung: DeskHub **1.0.52 oder neuer** (Berechtigungen `auth.login` und `auth.customer`, Anmelde-Adressen `/staff/plugin_login.php` und `/plugin_login.php`).
|
||||
|
||||
## Einrichtung
|
||||
|
||||
1. **In authentik** unter *Anwendungen → Provider* einen Provider vom Typ **OAuth2/OpenID** anlegen, Client-Typ *vertraulich*.
|
||||
2. **Umleitungs-URI** (Modus „Strict“): `https://<deine-DeskHub-Adresse>/staff/plugin_login.php?p=authentik&e=callback` (die genaue Adresse zeigt die Einstellungsseite).
|
||||
3. **Bereiche:** `openid`, `email`, `profile`; für die Gruppenprüfung zusätzlich ein Bereich, der `groups` liefert.
|
||||
4. Eine **Anwendung** mit diesem Provider anlegen und festlegen, wer sie nutzen darf.
|
||||
5. In DeskHub: Plugin hochladen und aktivieren (Berechtigungen `ui.menu`, `auth.login` und `auth.customer` bestätigen), dann im Adminportal unter **authentik** die Issuer-Adresse (`https://auth.example.de/application/o/<Anwendungs-Kürzel>`), Client-ID und Client-Geheimnis eintragen, „Anmeldung einschalten“, speichern, **Verbindung testen**.
|
||||
6. Mitarbeiter: Jeder hat sein DeskHub-Konto und verknüpft es einmal unter **Mein authentik-Konto** (oder meldet sich beim ersten Mal mit gleicher E-Mail-Adresse an). Kunden: unter „Kunden“ einschalten (siehe unten).
|
||||
|
||||
## Mitarbeiter
|
||||
|
||||
Angemeldet wird nur, wer in authentik bestätigt ist (E-Mail nicht als „nicht bestätigt“ markiert) **und** in DeskHub ein **aktives** Mitarbeiterkonto hat, das mit seinem authentik-Konto **verknüpft** ist. Die Verknüpfung macht jeder Mitarbeiter selbst unter **Mein authentik-Konto** (Menü) mit einem Klick; sie hängt an der festen Benutzerkennung von authentik, nicht an der E-Mail-Adresse. Ohne Verknüpfung klappt die erste Anmeldung auch, wenn die E-Mail-Adresse in authentik und DeskHub gleich ist; dabei wird automatisch verknüpft. Mit der Einstellung **Nur verknüpfte Konten** geht das nicht mehr. Hat ein DeskHub-Konto schon eine Verknüpfung, kann ein fremdes authentik-Konto mit gleicher E-Mail-Adresse nicht hinein. Administratoren sehen alle Verknüpfungen in den Einstellungen und können sie lösen. Konten werden nicht automatisch angelegt. Optional zusätzlich nur Mitglieder einer authentik-Gruppe. Die zweite Stufe von DeskHub (Authenticator-Code) entfällt bei dieser Anmeldung, weil authentik sie übernimmt. Jede Anmeldung und jede Ablehnung steht im Admin-Protokoll.
|
||||
|
||||
## Kunden (Ticketersteller)
|
||||
|
||||
In den Einstellungen unter „Kunden“ einschalten. Dann erscheint der Knopf auch auf der Anmeldeseite des Kundenportals. Es zählt **nur die in authentik bestätigte E-Mail-Adresse**: Gibt es ein Kundenkonto mit dieser Adresse, wird es angemeldet; gibt es keines und „automatisch anlegen“ ist an, legt DeskHub es an (Name aus authentik, **ohne Passwort**; wer eines will, nutzt „Passwort vergessen“). Gesperrte Adressen (Sperrliste) kommen nicht hinein. Optional nur Kunden einer eigenen authentik-Gruppe. Die Rückkehr-Adresse in authentik ist dieselbe wie für Mitarbeiter, Sie brauchen keine zweite. Neue Kundenkonten stehen im Admin-Protokoll.
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- PKCE (S256), zufälliger Zustandswert (`state`) und `nonce`, jeder Ablauf gilt nur einmal und höchstens 10 Minuten.
|
||||
- Das ID-Token kommt direkt vom Token-Endpunkt über eine **geprüfte HTTPS-Verbindung** (Zertifikat und Name werden geprüft, keine Weiterleitungen); geprüft werden Aussteller (`iss`), Zielgruppe (`aud`/`azp`), Ablauf (`exp`) und `nonce`. Eine zusätzliche Signaturprüfung ist dafür nach OIDC Core 3.1.3.7 nicht nötig.
|
||||
- **E-Mail nicht bestätigt:** Meldet authentik `email_verified = false`, ordnet DeskHub ein Konto nicht über die Adresse zu (nicht verknüpfte Mitarbeiter, Kunden). Das Verknüpfen unter „Mein authentik-Konto“ und die Anmeldung schon verknüpfter Konten gehen trotzdem. Ausschaltbar mit „Bestätigte E-Mail-Adresse verlangen“ (nur sinnvoll, wenn allein Sie in authentik Benutzer anlegen und Adressen ändern).
|
||||
- Adressen: nur `https` (`http` nur zu `localhost` zum Testen), ohne Zugangsdaten, Parameter und Anker.
|
||||
- Das Client-Geheimnis liegt verschlüsselt in der Datenbank und wird nie wieder angezeigt.
|
||||
- Fällt authentik aus, melden sich Mitarbeiter weiter mit E-Mail und Passwort an.
|
||||
|
||||
## Grenzen
|
||||
|
||||
- Mitarbeiterkonten werden nicht automatisch angelegt und es gibt keine Rollen- oder Abteilungszuordnung aus Gruppen. Kundenkonten entstehen nur auf Wunsch.
|
||||
- Kunden werden nur über die E-Mail-Adresse zugeordnet (keine feste Verknüpfung). Wer die E-Mail-Adresse in authentik ändern darf, könnte damit ein anderes Kundenkonto öffnen: Das in authentik absichern, wenn Kunden sich dort selbst registrieren dürfen.
|
||||
- Abmelden in DeskHub meldet nicht bei authentik ab (kein „Single Logout“).
|
||||
- Das Plugin schaltet die Anmeldung mit E-Mail und Passwort nicht ab.
|
||||
|
||||
## Tests
|
||||
|
||||
`tests/plugin_authentik.php` (OpenID-Ablauf mit nachgebautem authentik) und `tests/plugin_login.php` (Anmelde-Erweiterung des Servers) im DeskHub-Projekt.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Anmeldung mit authentik">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#0A5F95"/>
|
||||
<stop offset="1" stop-color="#0E8C7C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="256" height="256" rx="56" fill="url(#bg)"/>
|
||||
<path d="M128 40 L200 66 V122 C200 166 170 196 128 214 C86 196 56 166 56 122 V66 Z" fill="#ffffff" fill-opacity=".16" stroke="#ffffff" stroke-width="8" stroke-linejoin="round"/>
|
||||
<circle cx="128" cy="104" r="20" fill="#ffd79a"/>
|
||||
<path d="M128 124 V176 M128 148 H148 M128 166 H142" fill="none" stroke="#ffd79a" stroke-width="12" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 725 B |
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"_info": "English translations of the authentik plugin. Key = German source text, value = English. {} stands for a changing part, {#} for a number. Further languages: lang/<code>.json, for example lang/fr.json.",
|
||||
"(seit {}).": "(since {}).",
|
||||
". Alternativ klappt die erste Anmeldung auch, wenn die E-Mail-Adresse in authentik und DeskHub gleich ist (dann wird automatisch verknüpft).": ". Alternatively the first sign-in also works if the email address is the same in authentik and DeskHub (it is then linked automatically).",
|
||||
". Für die Gruppenprüfung zusätzlich den Bereich, der": ". For the group check, additionally add the scope that",
|
||||
"Angemeldet wird nur, wer in authentik bestätigt ist und in DeskHub ein": "Only someone who is confirmed in authentik and has",
|
||||
"Anmeldung mit authentik einschalten": "Switch on sign-in with authentik",
|
||||
"Anwendung": "application",
|
||||
"Anwendungen → Provider": "Applications → Providers",
|
||||
"Auch Kunden mit authentik anmelden.": "Also sign in customers with authentik.",
|
||||
"Auf der Anmeldeseite des Mitarbeiterbereichs erscheint der Knopf „{}“.": "The button “{}” appears on the sign-in page of the staff area.",
|
||||
"Auf diesem Server fehlt die PHP-Erweiterung cURL.": "The PHP extension cURL is missing on this server.",
|
||||
"Ausgeschaltet.": "Switched off.",
|
||||
"Bereiche (Scopes)": "Scopes",
|
||||
"Bereiche:": "Scopes:",
|
||||
"Beschriftung": "Label",
|
||||
"Beschriftung des Knopfes": "Button label",
|
||||
"Bestätigte E-Mail-Adresse verlangen (empfohlen).": "Require a verified email address (recommended).",
|
||||
"Bitte die Issuer-Adresse mit https:// angeben, ohne Parameter (zum Beispiel https://auth.example.de/application/o/deskhub).": "Please enter the issuer address with https://, without parameters (for example https://auth.example.com/application/o/deskhub).",
|
||||
"Bitte zuerst anmelden.": "Please sign in first.",
|
||||
"Bitte zuerst eine gültige Issuer-Adresse speichern.": "Please save a valid issuer address first.",
|
||||
"Client-Geheimnis (Client Secret)": "Client secret",
|
||||
"Client-ID": "Client ID",
|
||||
"Client-ID und Client-Geheimnis hier eintragen. Die": "Enter the client ID and client secret here. The",
|
||||
"Client-Typ:": "Client type:",
|
||||
"Danach führt „{}“ auf der Anmeldeseite genau in dieses Konto. Auch ohne Verknüpfung klappt die Anmeldung, wenn die E-Mail-Adresse in authentik und DeskHub gleich ist; dabei wird sie dann automatisch verknüpft.": "Afterwards “{}” on the sign-in page leads exactly into this account. Even without a link, signing in works if the email address is the same in authentik and DeskHub; it is then linked automatically.",
|
||||
"Danach führt „{}“ auf der Anmeldeseite genau in dieses Konto. Ohne Verknüpfung ist die Anmeldung mit authentik hier nicht möglich.": "Afterwards “{}” on the sign-in page leads exactly into this account. Without a link, signing in with authentik is not possible here.",
|
||||
"Das mit Ihrem authentik-Konto verknüpfte DeskHub-Konto ist deaktiviert.": "The DeskHub account linked to your authentik account is deactivated.",
|
||||
"Der Ablauf nutzt PKCE, einen Zustandswert und eine Nonce; Aussteller, Zielgruppe und Ablauf des ID-Tokens werden geprüft. Das Token kommt direkt von authentik über eine geprüfte HTTPS-Verbindung. Das Geheimnis liegt verschlüsselt in der Datenbank. Fällt authentik aus, melden sich Mitarbeiter weiter mit E-Mail und Passwort an. Jede Anmeldung steht im Admin-Protokoll.": "The flow uses PKCE, a state value and a nonce; issuer, audience and expiry of the ID token are checked. The token comes directly from authentik over a verified HTTPS connection. The secret is stored encrypted in the database. If authentik fails, staff keep signing in with email and password. Every sign-in is recorded in the admin log.",
|
||||
"Der DeskHub-Server kennt die Kunden-Anmeldung (Version 1.0.52 oder neuer).": "The DeskHub server supports customer sign-in (version 1.0.52 or newer).",
|
||||
"Der Issuer in der Antwort („{}“) passt nicht zur eingestellten Adresse.": "The issuer in the response (“{}”) does not match the configured address.",
|
||||
"Der Knopf erscheint auf der Anmeldeseite des Kundenportals. Es zählt nur die in authentik bestätigte E-Mail-Adresse: Hat ein Kundenkonto dieselbe Adresse, meldet DeskHub es an.": "The button appears on the sign-in page of the customer portal. Only the email address confirmed in authentik counts: if a customer account has the same address, DeskHub signs it in.",
|
||||
"Der Knopf erscheint dort noch nicht.": "The button does not appear there yet.",
|
||||
"Der Knopf erscheint erst nach dem Einschalten.": "The button only appears after switching on.",
|
||||
"Der Server ist zu alt: Bitte DeskHub 1.0.52 oder neuer einspielen (auch login.php, plugin_login.php und includes/plugins.php).": "The server is too old: please install DeskHub 1.0.52 or newer (including login.php, plugin_login.php and includes/plugins.php).",
|
||||
"Die Anmeldeseite des Kundenportals (login.php) ist auf dem neuesten Stand.": "The sign-in page of the customer portal (login.php) is up to date.",
|
||||
"Die Anmeldung ist abgelaufen oder wurde nicht hier gestartet. Bitte noch einmal auf „Mit authentik verknüpfen“ klicken.": "The sign-in has expired or was not started here. Please click “Link with authentik” again.",
|
||||
"Die Anmeldung ist abgelaufen oder wurde nicht hier gestartet. Bitte noch einmal auf „{}“ klicken.": "The sign-in has expired or was not started here. Please click “{}” again.",
|
||||
"Die Anmeldung mit authentik ist für Kunden nicht eingeschaltet.": "Sign-in with authentik is not switched on for customers.",
|
||||
"Die Anmeldung mit authentik ist nicht eingerichtet.": "Sign-in with authentik is not set up.",
|
||||
"Die Anmeldung mit authentik ist nicht eingerichtet. Ein Administrator kann sie unter „authentik“ einschalten.": "Sign-in with authentik is not set up. An administrator can switch it on under “authentik”.",
|
||||
"Die Antwort stammt nicht vom eingestellten authentik (Aussteller).": "The response does not come from the configured authentik (issuer).",
|
||||
"Die Antwort von authentik enthält keine Benutzerkennung.": "The response from authentik contains no user identifier.",
|
||||
"Die Antwort von authentik gehört nicht zu dieser Anmeldung (Nonce).": "The response from authentik does not belong to this sign-in (nonce).",
|
||||
"Die Antwort von authentik gilt für eine andere Anwendung.": "The response from authentik is for a different application.",
|
||||
"Die Antwort von authentik ist unlesbar.": "The response from authentik is unreadable.",
|
||||
"Die Antwort von authentik passt nicht zur gestarteten Anmeldung. Bitte noch einmal versuchen.": "The response from authentik does not match the sign-in that was started. Please try again.",
|
||||
"Die Berechtigung „auth.customer“ fehlt: Unter Plugins das Plugin deaktivieren und wieder aktivieren und die Berechtigungen bestätigen.": "The permission “auth.customer” is missing: under Plugins, deactivate and reactivate the plugin and confirm the permissions.",
|
||||
"Die Berechtigung „auth.customer“ ist freigegeben.": "The permission “auth.customer” is approved.",
|
||||
"Die Bereiche (Scopes) enthalten ungültige Zeichen.": "The scopes contain invalid characters.",
|
||||
"Die Bestätigung von authentik ist abgelaufen.": "The confirmation from authentik has expired.",
|
||||
"Die Client-ID ist ungültig.": "The client ID is invalid.",
|
||||
"Die Datei login.php im DeskHub-Hauptordner ist alt und zeigt keine Plugin-Knöpfe: Bitte die aktuelle login.php hochladen.": "The file login.php in the main DeskHub folder is outdated and shows no plugin buttons: please upload the current login.php.",
|
||||
"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.": "The email address is not verified in authentik, so DeskHub does not assign the account by address. Link your account once under “My authentik account” (after signing in with email and password); after that signing in with authentik works.",
|
||||
"Die E-Mail-Adresse ist in authentik nicht bestätigt. Bitte erst in authentik bestätigen.": "The email address is not verified in authentik. Please verify it in authentik first.",
|
||||
"Die Konfiguration von authentik enthält keinen gültigen Eintrag „{}“.": "The configuration from authentik has no valid entry “{}”.",
|
||||
"Die Verbindung ist nicht vollständig (Issuer, Client-ID, Geheimnis, eingeschaltet).": "The connection is incomplete (issuer, client ID, secret, switched on).",
|
||||
"Die Verbindung zu authentik ist eingerichtet und eingeschaltet.": "The connection to authentik is set up and switched on.",
|
||||
"Die Verknüpfung ist gelöst.": "The link has been removed.",
|
||||
"Die Verknüpfung ist gelöst. Mit authentik melden Sie sich erst wieder an, wenn Sie es neu verknüpfen (oder, falls erlaubt, über die E-Mail-Adresse).": "The link has been removed. You can only sign in with authentik again after linking it anew (or, if allowed, via the email address).",
|
||||
"Die Verknüpfung wurde von einem anderen Konto gestartet. Bitte noch einmal starten.": "The link was started by a different account. Please start it again.",
|
||||
"Dieses DeskHub-Konto ist mit einem anderen authentik-Konto verknüpft.": "This DeskHub account is linked to a different authentik account.",
|
||||
"Dieses authentik-Konto ist bereits mit einem anderen DeskHub-Konto verknüpft.": "This authentik account is already linked to a different DeskHub account.",
|
||||
"E-Mail in DeskHub": "Email in DeskHub",
|
||||
"E-Mail in authentik": "Email in authentik",
|
||||
"Eine": "Create an",
|
||||
"Eingeschaltet, aber unvollständig.": "Switched on, but incomplete.",
|
||||
"Einrichtung in authentik": "Setup in authentik",
|
||||
"Für jeden Mitarbeiter in DeskHub ein Konto mit derselben E-Mail-Adresse wie in authentik haben.": "Every staff member needs an account in DeskHub with the same email address as in authentik.",
|
||||
"Für {} 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.": "There is no active staff account in DeskHub for {}. Please ask an administrator to create it with the same email address, or link an existing account under “My authentik account”.",
|
||||
"Für {} gibt es in DeskHub kein aktives Mitarbeiterkonto. Bitte einen Administrator bitten, es mit derselben E-Mail-Adresse anzulegen.": "There is no active staff account in DeskHub for {}. Please ask an administrator to create it with the same email address.",
|
||||
"Gilt, wo die Adresse über das Konto entscheidet: bei Kunden und bei Mitarbeitern, die noch nicht verknüpft sind. Meldet authentik die Adresse als „nicht bestätigt“, kommen sie sonst nicht hinein. Wer schon verknüpft ist, meldet sich immer an. Nur ausschalten, wenn nur du in authentik Benutzer anlegst und Adressen ändern darfst.": "Applies where the address decides about the account: for customers and for staff who are not linked yet. If authentik reports the address as “not verified”, they cannot get in otherwise. Anyone who is already linked always signs in. Only switch off if you are the only one who creates users and changes addresses in authentik.",
|
||||
"Ihr DeskHub-Konto gehört zum authentik-Konto": "Your DeskHub account belongs to the authentik account",
|
||||
"Ihr authentik-Konto ({}) ist jetzt verknüpft. Die Anmeldung mit authentik führt künftig genau in dieses Konto.": "Your authentik account ({}) is now linked. Signing in with authentik now leads exactly into this account.",
|
||||
"Ihr authentik-Konto gehört nicht zur Gruppe „{}“, die für DeskHub nötig ist.": "Your authentik account does not belong to the group “{}” that DeskHub requires.",
|
||||
"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“.": "Your authentik account is not assigned to a DeskHub account yet. Sign in with email and password and link it there under “My authentik account”.",
|
||||
"In authentik unter": "In authentik, under",
|
||||
"Issuer-Adresse": "issuer address",
|
||||
"Issuer-Adresse (OpenID-Konfiguration)": "Issuer address (OpenID configuration)",
|
||||
"Issuer-Adresse, Client-ID und Geheimnis müssen eingetragen sein.": "The issuer address, client ID and secret must be entered.",
|
||||
"Jeder Mitarbeiter hat sein DeskHub-Konto und verknüpft es einmal selbst unter": "Every staff member has their DeskHub account and links it once themselves under",
|
||||
"Keine Berechtigung.": "No permission.",
|
||||
"Kunden (Ticketersteller)": "Customers (ticket creators)",
|
||||
"Kunden-Anmeldung": "Customer sign-in",
|
||||
"Kunden-Anmeldung ist ausgeschaltet: Unten unter „Kunden“ den Haken setzen und speichern.": "Customer sign-in is switched off: tick the box under “Customers” below and save.",
|
||||
"Kunden-Anmeldung ist eingeschaltet („Auch Kunden mit authentik anmelden“).": "Customer sign-in is switched on (“Also sign in customers with authentik”).",
|
||||
"Kundenkonto automatisch anlegen,": "Create the customer account automatically",
|
||||
"Lösen": "Remove",
|
||||
"Mein authentik-Konto": "My authentik account",
|
||||
"Mit authentik anmelden": "Sign in with authentik",
|
||||
"Mit authentik verknüpfen": "Link with authentik",
|
||||
"Mit einem anderen authentik-Konto neu verknüpfen": "Link again with a different authentik account",
|
||||
"Mit „{}“ auf der Anmeldeseite kommen Sie damit direkt in dieses Konto, auch wenn sich die E-Mail-Adresse in authentik oder DeskHub später ändert. Ein anderes authentik-Konto kann dieses DeskHub-Konto nicht öffnen.": "With “{}” on the sign-in page you now go straight into this account, even if the email address in authentik or DeskHub changes later. No other authentik account can open this DeskHub account.",
|
||||
"Mitarbeiter": "Staff member",
|
||||
"Mitarbeiter und Kunden melden sich mit authentik an (OpenID Connect, mit PKCE): Knopf „Mit authentik anmelden“ auf der Anmeldeseite des Mitarbeiterbereichs und, wenn eingeschaltet, des Kundenportals. Mitarbeiter: Angemeldet wird nur, wer in authentik bestätigt ist und in DeskHub ein aktives, verknüpftes Mitarbeiterkonto (oder eines mit derselben E-Mail-Adresse) hat; sie verknüpfen ihr Konto selbst unter „Mein authentik-Konto“. Kunden (Ticketersteller): Die bestätigte E-Mail-Adresse muss passen, das Konto wird bei Bedarf automatisch angelegt. Optional nur Mitglieder bestimmter authentik-Gruppen. E-Mail und Passwort bleiben als Zugang erhalten.": "Staff and customers sign in with authentik (OpenID Connect, with PKCE): a “Sign in with authentik” button on the sign-in page of the staff area and, if switched on, of the customer portal. Staff: only someone who is confirmed in authentik and has an active, linked staff account (or one with the same email address) in DeskHub is signed in; they link their account themselves under “My authentik account”. Customers (ticket creators): the confirmed email address must match, and the account is created automatically if wanted. Optionally restricted to authentik groups. Email and password remain available as a way in.",
|
||||
"Noch kein Mitarbeiter hat sein Konto verknüpft. Mitarbeiter tun das selbst unter „Mein authentik-Konto“; bei der ersten Anmeldung über die E-Mail-Adresse geschieht es automatisch.": "No staff member has linked their account yet. Staff do this themselves under “My authentik account”; it happens automatically on the first sign-in via the email address.",
|
||||
"Noch nicht verknüpft.": "Not linked yet.",
|
||||
"Nur Kunden aus dieser authentik-Gruppe (optional)": "Only customers from this authentik group (optional)",
|
||||
"Nur Mitglieder dieser authentik-Gruppe (optional)": "Only members of this authentik group (optional)",
|
||||
"Nur verknüpfte Konten anmelden.": "Only sign in linked accounts.",
|
||||
"Ohne Haken klappt die Anmeldung auch über die E-Mail-Adresse (das Konto wird dabei gleich verknüpft). Mit Haken muss jeder Mitarbeiter sein Konto erst unter „Mein authentik-Konto“ verknüpfen.": "Without the tick, signing in also works via the email address (the account is linked at the same time). With the tick, every staff member must first link their account under “My authentik account”.",
|
||||
"Ruft die Konfiguration von authentik ab (ohne Anmeldung). Vorher speichern.": "Fetches the configuration from authentik (without signing in). Save first.",
|
||||
"Seit": "Since",
|
||||
"Sicherheit:": "Security:",
|
||||
"Umleitungs-URI (Redirect URI)": "Redirect URI",
|
||||
"Ungültige Adresse.": "Invalid address.",
|
||||
"Verbindung in Ordnung: authentik meldet den Aussteller „{}“ und die Anmelde-Adresse {}.": "Connection OK: authentik reports the issuer “{}” and the sign-in address {}.",
|
||||
"Verbindung testen": "Test connection",
|
||||
"Verbindung zu authentik": "Connection to authentik",
|
||||
"Verknüpfen Sie Ihr DeskHub-Konto einmal mit Ihrem authentik-Konto. Sie melden sich dazu kurz bei authentik an und kommen hierher zurück.": "Link your DeskHub account once with your authentik account. You briefly sign in at authentik and come back here.",
|
||||
"Verknüpfte Konten ({#})": "Linked accounts ({})",
|
||||
"Verknüpfung dieses Mitarbeiters lösen?": "Remove this staff member's link?",
|
||||
"Verknüpfung lösen": "Remove link",
|
||||
"Verknüpfung wirklich lösen?": "Really remove the link?",
|
||||
"Zustand": "Status",
|
||||
"aktives Mitarbeiterkonto": "an active staff account",
|
||||
"aktives Mitarbeiterkonto mit derselben E-Mail-Adresse": "an active staff account with the same email address",
|
||||
"anlegen.": "create.",
|
||||
"authentik antwortet nicht mit einer gültigen Konfiguration (HTTP {}). Stimmt die Issuer-Adresse?": "authentik does not respond with a valid configuration (HTTP {}). Is the issuer address correct?",
|
||||
"authentik hat den Anmelde-Code nicht angenommen (HTTP {}). Stimmen Client-ID und Geheimnis?": "authentik did not accept the sign-in code (HTTP {}). Are the client ID and secret correct?",
|
||||
"authentik hat die Anmeldung nicht bestätigt ({}).": "authentik did not confirm the sign-in ({}).",
|
||||
"authentik hat keine E-Mail-Adresse geliefert. Im Provider den Bereich „email“ aktivieren.": "authentik did not provide an email address. Enable the scope “email” on the provider.",
|
||||
"authentik ist nicht erreichbar: {}": "authentik is not reachable: {}",
|
||||
"authentik-Anmeldung": "authentik sign-in",
|
||||
"authentik-Anmeldung (OpenID Connect)": "authentik sign-in (OpenID Connect)",
|
||||
"einen Provider vom Typ": "create a provider of type",
|
||||
"genau so eintragen (Modus „Strict“):": "enter it exactly like this (mode “Strict”):",
|
||||
"hat, das mit seinem authentik-Konto": "in DeskHub that is",
|
||||
"hat. Konten werden nicht automatisch angelegt. E-Mail und Passwort funktionieren weiter. Die zweite Stufe (Authenticator-Code) von DeskHub entfällt bei dieser Anmeldung: Dafür sorgt authentik.": "in DeskHub is signed in. Accounts are not created automatically. Email and password keep working. DeskHub's second step (authenticator code) is skipped for this sign-in: authentik takes care of that.",
|
||||
"ist (oder dieselbe E-Mail-Adresse hat). Konten werden nicht automatisch angelegt. E-Mail und Passwort funktionieren weiter. Die zweite Stufe (Authenticator-Code) von DeskHub entfällt bei dieser Anmeldung: Dafür sorgt authentik.": "to their authentik account (or has the same email address) is signed in. Accounts are not created automatically. Email and password keep working. DeskHub's second step (authenticator code) is skipped for this sign-in: authentik takes care of that.",
|
||||
"liefert (in authentik: Bereichs-Zuordnung „groups“).": "provides (in authentik: scope mapping “groups”).",
|
||||
"mindestens": "at least",
|
||||
"mit diesem Provider anlegen und bestimmen, wer sie nutzen darf (authentik prüft das selbst).": "with this provider and decide who may use it (authentik checks this itself).",
|
||||
"steht beim Provider unter „OpenID-Konfigurations-Aussteller“, sie sieht so aus:": "is shown on the provider under “OpenID Configuration Issuer”; it looks like this:",
|
||||
"verknüpft": "linked",
|
||||
"vertraulich (Confidential).": "confidential.",
|
||||
"wenn es zu der E-Mail-Adresse noch keines gibt (ohne Passwort; wer eines möchte, nutzt „Passwort vergessen“). Ohne Haken kommen nur Kunden hinein, die schon ein Konto haben.": "if there is none for the email address yet (without a password; anyone who wants one uses “Forgot password”). Without the tick only customers who already have an account get in.",
|
||||
"zum Beispiel deskhub-kunden": "for example deskhub-customers",
|
||||
"zum Beispiel deskhub-support": "for example deskhub-support",
|
||||
"✓ Der Knopf erscheint auf der Anmeldeseite des Kundenportals.": "✓ The button appears on the sign-in page of the customer portal.",
|
||||
"✓ Eingeschaltet.": "✓ Switched on.",
|
||||
"✓ Verknüpft.": "✓ Linked."
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
<?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'];
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (($staff['role'] ?? '') !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo '<p>Keine Berechtigung.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$message = '';
|
||||
$messageType = 'success';
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||
verify_csrf();
|
||||
$action = (string) ($_POST['action'] ?? '');
|
||||
if ($action === 'save') {
|
||||
$issuer = rtrim(trim((string) ($_POST['issuer'] ?? '')), '/');
|
||||
$clientId = trim((string) ($_POST['client_id'] ?? ''));
|
||||
$secretIn = (string) ($_POST['client_secret'] ?? '');
|
||||
$scopes = trim(preg_replace('/\s+/', ' ', (string) ($_POST['scopes'] ?? '')));
|
||||
if ($issuer !== '' && !authentik_url_ok($issuer)) {
|
||||
$message = 'Bitte die Issuer-Adresse mit https:// angeben, ohne Parameter (zum Beispiel https://auth.example.de/application/o/deskhub).';
|
||||
$messageType = 'error';
|
||||
} elseif (strlen($clientId) > 200 || preg_match('/[\x00-\x20]/', $clientId)) {
|
||||
$message = 'Die Client-ID ist ungültig.';
|
||||
$messageType = 'error';
|
||||
} elseif ($scopes !== '' && !preg_match('/^[A-Za-z0-9_.:\- ]{1,200}$/', $scopes)) {
|
||||
$message = 'Die Bereiche (Scopes) enthalten ungültige Zeichen.';
|
||||
$messageType = 'error';
|
||||
} else {
|
||||
plugin_set_setting(AUTHENTIK, 'issuer', $issuer);
|
||||
plugin_set_setting(AUTHENTIK, 'client_id', $clientId);
|
||||
if ($secretIn !== '') plugin_set_setting(AUTHENTIK, 'client_secret_enc', encrypt_secret($secretIn));
|
||||
plugin_set_setting(AUTHENTIK, 'scopes', $scopes !== '' ? $scopes : 'openid email profile');
|
||||
plugin_set_setting(AUTHENTIK, 'group', mb_substr(trim((string) ($_POST['group'] ?? '')), 0, 100));
|
||||
plugin_set_setting(AUTHENTIK, 'label', mb_substr(trim((string) ($_POST['label'] ?? '')), 0, 60));
|
||||
plugin_set_setting(AUTHENTIK, 'enabled', !empty($_POST['enabled']) ? '1' : '0');
|
||||
plugin_set_setting(AUTHENTIK, 'only_linked', !empty($_POST['only_linked']) ? '1' : '0');
|
||||
plugin_set_setting(AUTHENTIK, 'require_verified', !empty($_POST['require_verified']) ? '1' : '0');
|
||||
plugin_set_setting(AUTHENTIK, 'customers', !empty($_POST['customers']) ? '1' : '0');
|
||||
plugin_set_setting(AUTHENTIK, 'customer_create', !empty($_POST['customer_create']) ? '1' : '0');
|
||||
plugin_set_setting(AUTHENTIK, 'customer_group', mb_substr(trim((string) ($_POST['customer_group'] ?? '')), 0, 100));
|
||||
plugin_set_setting(AUTHENTIK, 'discovery', ''); // neu laden
|
||||
audit_log((int) $staff['id'], 'authentik-Anmeldung: Einstellungen geändert' . (!empty($_POST['enabled']) ? ' (eingeschaltet)' : ' (ausgeschaltet)'));
|
||||
$message = 'Gespeichert.';
|
||||
}
|
||||
} elseif ($action === 'unlink_staff') {
|
||||
$sid = (int) ($_POST['staff_id'] ?? 0);
|
||||
if ($sid > 0 && authentik_link_of($sid) !== null) {
|
||||
authentik_link_remove($sid);
|
||||
audit_log((int) $staff['id'], 'authentik: Verknüpfung von Mitarbeiter #' . $sid . ' gelöst');
|
||||
$message = 'Die Verknüpfung ist gelöst.';
|
||||
}
|
||||
} elseif ($action === 'test') {
|
||||
$cfg = authentik_config();
|
||||
if (!authentik_url_ok($cfg['issuer'])) {
|
||||
$message = 'Bitte zuerst eine gültige Issuer-Adresse speichern.';
|
||||
$messageType = 'error';
|
||||
} else {
|
||||
$d = authentik_discovery($cfg['issuer'], true);
|
||||
if ($d['ok']) {
|
||||
$message = 'Verbindung in Ordnung: authentik meldet den Aussteller „' . $d['doc']['issuer'] . '“ und die Anmelde-Adresse ' . $d['doc']['authorization_endpoint'] . '.';
|
||||
} else {
|
||||
$message = $d['error'];
|
||||
$messageType = 'error';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cfg = authentik_config();
|
||||
$hasSecret = $cfg['client_secret'] !== '';
|
||||
$ready = authentik_ready();
|
||||
$redirect = authentik_redirect_uri();
|
||||
?>
|
||||
<h1>authentik-Anmeldung</h1>
|
||||
<?php if ($message !== ''): ?><div class="flash <?= $messageType === 'error' ? 'error' : 'success' ?>" role="status"><?= e($message) ?></div><?php endif; ?>
|
||||
|
||||
<section class="card" style="max-width:900px">
|
||||
<h3>Zustand</h3>
|
||||
<p>
|
||||
<?php if ($ready): ?><strong style="color:#157a45">✓ Eingeschaltet.</strong> Auf der Anmeldeseite des Mitarbeiterbereichs erscheint der Knopf „<?= e($cfg['label']) ?>“.
|
||||
<?php elseif ($cfg['enabled']): ?><strong style="color:#b45309">Eingeschaltet, aber unvollständig.</strong> Issuer-Adresse, Client-ID und Geheimnis müssen eingetragen sein.
|
||||
<?php else: ?><strong style="color:#b45309">Ausgeschaltet.</strong> Der Knopf erscheint erst nach dem Einschalten.<?php endif; ?>
|
||||
</p>
|
||||
<p class="muted" style="font-size:.88rem">Angemeldet wird nur, wer in authentik bestätigt ist und in DeskHub ein <strong>aktives Mitarbeiterkonto</strong> hat, das mit seinem authentik-Konto <strong>verknüpft</strong> ist (oder dieselbe E-Mail-Adresse hat). Konten werden nicht automatisch angelegt. E-Mail und Passwort funktionieren weiter. Die zweite Stufe (Authenticator-Code) von DeskHub entfällt bei dieser Anmeldung: Dafür sorgt authentik.</p>
|
||||
<?php
|
||||
// Warum erscheint (oder fehlt) der Knopf auf der Anmeldeseite des Kundenportals?
|
||||
$serverOk = function_exists('plugin_register_customer_login');
|
||||
$permOk = in_array('auth.customer', plugin_state()['perms'][AUTHENTIK] ?? [], true);
|
||||
$custRegistered = isset(plugin_state()['customer_logins'][AUTHENTIK]['start']);
|
||||
$checks = [
|
||||
[$serverOk, 'Der DeskHub-Server kennt die Kunden-Anmeldung (Version 1.0.52 oder neuer).', 'Der Server ist zu alt: Bitte DeskHub 1.0.52 oder neuer einspielen (auch login.php, plugin_login.php und includes/plugins.php).'],
|
||||
[$permOk, 'Die Berechtigung „auth.customer“ ist freigegeben.', 'Die Berechtigung „auth.customer“ fehlt: Unter Plugins das Plugin deaktivieren und wieder aktivieren und die Berechtigungen bestätigen.'],
|
||||
[$cfg['customers'], 'Kunden-Anmeldung ist eingeschaltet („Auch Kunden mit authentik anmelden“).', 'Kunden-Anmeldung ist ausgeschaltet: Unten unter „Kunden“ den Haken setzen und speichern.'],
|
||||
[$ready, 'Die Verbindung zu authentik ist eingerichtet und eingeschaltet.', 'Die Verbindung ist nicht vollständig (Issuer, Client-ID, Geheimnis, eingeschaltet).'],
|
||||
];
|
||||
// Zeichnet die Kunden-Anmeldeseite (login.php im DeskHub-Hauptordner) die Knöpfe der Plugins überhaupt? (Datei fehlt oder ist alt: Knopf bleibt weg)
|
||||
$loginFile = dirname(plugins_dir()) . '/login.php';
|
||||
$loginPhp = is_file($loginFile) ? (string) @file_get_contents($loginFile) : null;
|
||||
$loginOk = $loginPhp === null ? true : str_contains($loginPhp, 'customer.login.buttons');
|
||||
$checks[] = [$loginOk, 'Die Anmeldeseite des Kundenportals (login.php) ist auf dem neuesten Stand.', 'Die Datei login.php im DeskHub-Hauptordner ist alt und zeigt keine Plugin-Knöpfe: Bitte die aktuelle login.php hochladen.'];
|
||||
$allOk = $serverOk && $permOk && $cfg['customers'] && $ready && $custRegistered && $loginOk;
|
||||
?>
|
||||
<h4 style="margin:14px 0 6px">Kunden-Anmeldung</h4>
|
||||
<p style="margin:0 0 6px"><?php if ($allOk): ?><strong style="color:#157a45">✓ Der Knopf erscheint auf der Anmeldeseite des Kundenportals.</strong><?php else: ?><strong style="color:#b45309">Der Knopf erscheint dort noch nicht.</strong><?php endif; ?></p>
|
||||
<ul style="list-style:none;padding:0;margin:0;font-size:.88rem">
|
||||
<?php foreach ($checks as [$ok, $yes, $no]): ?><li><?= $ok ? '<span style="color:#157a45">✓</span> ' . e($yes) : '<span style="color:#b91c1c">✗</span> ' . e($no) ?></li><?php endforeach; ?>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<form method="post" class="card" style="max-width:900px;margin-top:16px">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="save">
|
||||
<h3>Verbindung zu authentik</h3>
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="checkbox" name="enabled" value="1" style="width:auto" <?= $cfg['enabled'] ? 'checked' : '' ?>> Anmeldung mit authentik einschalten</label>
|
||||
<label for="issuer">Issuer-Adresse (OpenID-Konfiguration)</label>
|
||||
<input type="text" inputmode="url" id="issuer" name="issuer" value="<?= e($cfg['issuer']) ?>" placeholder="https://auth.example.de/application/o/deskhub" autocomplete="off">
|
||||
<label for="client_id">Client-ID</label>
|
||||
<input type="text" id="client_id" name="client_id" value="<?= e($cfg['client_id']) ?>" autocomplete="off">
|
||||
<label for="client_secret">Client-Geheimnis (Client Secret)</label>
|
||||
<input type="password" id="client_secret" name="client_secret" value="" placeholder="<?= $hasSecret ? '(unverändert)' : '' ?>" autocomplete="new-password">
|
||||
<label for="scopes">Bereiche (Scopes)</label>
|
||||
<input type="text" id="scopes" name="scopes" value="<?= e($cfg['scopes']) ?>">
|
||||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin-top:10px"><input type="checkbox" name="only_linked" value="1" style="width:auto;margin-top:3px" <?= plugin_setting(AUTHENTIK, 'only_linked', '0') === '1' ? 'checked' : '' ?>> <span><strong>Nur verknüpfte Konten anmelden.</strong> Ohne Haken klappt die Anmeldung auch über die E-Mail-Adresse (das Konto wird dabei gleich verknüpft). Mit Haken muss jeder Mitarbeiter sein Konto erst unter „Mein authentik-Konto“ verknüpfen.</span></label>
|
||||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin-top:10px"><input type="checkbox" name="require_verified" value="1" style="width:auto;margin-top:3px" <?= $cfg['require_verified'] ? 'checked' : '' ?>> <span><strong>Bestätigte E-Mail-Adresse verlangen (empfohlen).</strong> Gilt, wo die Adresse über das Konto entscheidet: bei Kunden und bei Mitarbeitern, die noch nicht verknüpft sind. Meldet authentik die Adresse als „nicht bestätigt“, kommen sie sonst nicht hinein. Wer schon verknüpft ist, meldet sich immer an. Nur ausschalten, wenn nur du in authentik Benutzer anlegst und Adressen ändern darfst.</span></label>
|
||||
<label for="group">Nur Mitglieder dieser authentik-Gruppe (optional)</label>
|
||||
<input type="text" id="group" name="group" value="<?= e($cfg['group']) ?>" placeholder="zum Beispiel deskhub-support">
|
||||
<h3 style="margin-top:22px">Kunden (Ticketersteller)</h3>
|
||||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400"><input type="checkbox" name="customers" value="1" style="width:auto;margin-top:3px" <?= $cfg['customers'] ? 'checked' : '' ?>> <span><strong>Auch Kunden mit authentik anmelden.</strong> Der Knopf erscheint auf der Anmeldeseite des Kundenportals. Es zählt nur die in authentik bestätigte E-Mail-Adresse: Hat ein Kundenkonto dieselbe Adresse, meldet DeskHub es an.</span></label>
|
||||
<label style="display:flex;gap:8px;align-items:flex-start;font-weight:400;margin-top:10px"><input type="checkbox" name="customer_create" value="1" style="width:auto;margin-top:3px" <?= $cfg['customer_create'] ? 'checked' : '' ?>> <span><strong>Kundenkonto automatisch anlegen,</strong> wenn es zu der E-Mail-Adresse noch keines gibt (ohne Passwort; wer eines möchte, nutzt „Passwort vergessen“). Ohne Haken kommen nur Kunden hinein, die schon ein Konto haben.</span></label>
|
||||
<label for="customer_group">Nur Kunden aus dieser authentik-Gruppe (optional)</label>
|
||||
<input type="text" id="customer_group" name="customer_group" value="<?= e($cfg['customer_group']) ?>" placeholder="zum Beispiel deskhub-kunden">
|
||||
<h3 style="margin-top:22px">Beschriftung</h3>
|
||||
<label for="label">Beschriftung des Knopfes</label>
|
||||
<input type="text" id="label" name="label" value="<?= e($cfg['label']) ?>">
|
||||
<div class="btn-row"><button type="submit" class="btn">Speichern</button></div>
|
||||
</form>
|
||||
|
||||
<form method="post" class="card" style="max-width:900px;margin-top:16px">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="test">
|
||||
<h3>Verbindung testen</h3>
|
||||
<p class="muted" style="font-size:.88rem">Ruft die Konfiguration von authentik ab (ohne Anmeldung). Vorher speichern.</p>
|
||||
<button type="submit" class="btn secondary">Verbindung testen</button>
|
||||
</form>
|
||||
|
||||
<?php $links = authentik_links_all(); ?>
|
||||
<section class="card" style="max-width:900px;margin-top:16px">
|
||||
<h3>Verknüpfte Konten (<?= count($links) ?>)</h3>
|
||||
<?php if (!$links): ?>
|
||||
<p class="muted">Noch kein Mitarbeiter hat sein Konto verknüpft. Mitarbeiter tun das selbst unter „Mein authentik-Konto“; bei der ersten Anmeldung über die E-Mail-Adresse geschieht es automatisch.</p>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x:auto"><table style="width:100%">
|
||||
<thead><tr><th>Mitarbeiter</th><th>E-Mail in DeskHub</th><th>E-Mail in authentik</th><th>Seit</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($links as $l): ?>
|
||||
<tr><td><?= e($l['name']) ?></td><td><?= e($l['staff_email']) ?></td><td><?= e($l['email']) ?></td><td><?= e(format_dt($l['at'])) ?></td>
|
||||
<td><form method="post" data-confirm="Verknüpfung dieses Mitarbeiters lösen?" onsubmit="return confirm(this.dataset.confirm)"><?= csrf_field() ?><input type="hidden" name="action" value="unlink_staff"><input type="hidden" name="staff_id" value="<?= (int) $l['staff_id'] ?>"><button class="btn secondary" style="padding:3px 10px;font-size:.8rem" type="submit">Lösen</button></form></td></tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card" style="max-width:900px;margin-top:16px">
|
||||
<h3>Einrichtung in authentik</h3>
|
||||
<ol style="line-height:1.7;padding-left:20px;font-size:.92rem">
|
||||
<li>In authentik unter <strong>Anwendungen → Provider</strong> einen Provider vom Typ <strong>OAuth2/OpenID</strong> anlegen. <strong>Client-Typ:</strong> vertraulich (Confidential).</li>
|
||||
<li><strong>Umleitungs-URI (Redirect URI)</strong> genau so eintragen (Modus „Strict“): <code><?= e($redirect) ?></code></li>
|
||||
<li><strong>Bereiche:</strong> mindestens <code>openid</code>, <code>email</code> und <code>profile</code>. Für die Gruppenprüfung zusätzlich den Bereich, der <code>groups</code> liefert (in authentik: Bereichs-Zuordnung „groups“).</li>
|
||||
<li>Eine <strong>Anwendung</strong> mit diesem Provider anlegen und bestimmen, wer sie nutzen darf (authentik prüft das selbst).</li>
|
||||
<li>Client-ID und Client-Geheimnis hier eintragen. Die <strong>Issuer-Adresse</strong> steht beim Provider unter „OpenID-Konfigurations-Aussteller“, sie sieht so aus: <code>https://auth.example.de/application/o/<Anwendungs-Kürzel>/</code>.</li>
|
||||
<li>Jeder Mitarbeiter hat sein DeskHub-Konto und verknüpft es einmal selbst unter <strong>Mein authentik-Konto</strong>. Alternativ klappt die erste Anmeldung auch, wenn die E-Mail-Adresse in authentik und DeskHub gleich ist (dann wird automatisch verknüpft).</li>
|
||||
</ol>
|
||||
<p class="muted" style="font-size:.85rem"><strong>Sicherheit:</strong> Der Ablauf nutzt PKCE, einen Zustandswert und eine Nonce; Aussteller, Zielgruppe und Ablauf des ID-Tokens werden geprüft. Das Token kommt direkt von authentik über eine geprüfte HTTPS-Verbindung. Das Geheimnis liegt verschlüsselt in der Datenbank. Fällt authentik aus, melden sich Mitarbeiter weiter mit E-Mail und Passwort an. Jede Anmeldung steht im Admin-Protokoll.</p>
|
||||
</section>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// „Mein authentik-Konto“: jeder Mitarbeiter verknüpft sein DeskHub-Konto selbst mit seinem authentik-Konto.
|
||||
$staffId = (int) ($staff['id'] ?? 0);
|
||||
$message = '';
|
||||
$messageType = 'success';
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
||||
verify_csrf();
|
||||
if (($_POST['action'] ?? '') === 'unlink' && authentik_link_of($staffId) !== null) {
|
||||
authentik_link_remove($staffId);
|
||||
audit_log($staffId, 'authentik: Verknüpfung gelöst');
|
||||
$message = 'Die Verknüpfung ist gelöst. Mit authentik melden Sie sich erst wieder an, wenn Sie es neu verknüpfen (oder, falls erlaubt, über die E-Mail-Adresse).';
|
||||
}
|
||||
}
|
||||
$ready = authentik_ready();
|
||||
$link = authentik_link_of($staffId);
|
||||
$onlyLinked = plugin_setting(AUTHENTIK, 'only_linked', '0') === '1';
|
||||
?>
|
||||
<h1>Mein authentik-Konto</h1>
|
||||
<?php if ($message !== ''): ?><div class="flash <?= $messageType === 'error' ? 'error' : 'success' ?>" role="status"><?= e($message) ?></div><?php endif; ?>
|
||||
|
||||
<section class="card" style="max-width:760px">
|
||||
<?php if (!$ready): ?>
|
||||
<p>Die Anmeldung mit authentik ist nicht eingerichtet. Ein Administrator kann sie unter „authentik“ einschalten.</p>
|
||||
<?php elseif ($link): ?>
|
||||
<p><strong style="color:#157a45">✓ Verknüpft.</strong> Ihr DeskHub-Konto gehört zum authentik-Konto <strong><?= e((string) ($link['email'] ?? '')) ?></strong> (seit <?= e(format_dt((string) ($link['at'] ?? ''))) ?>).</p>
|
||||
<p class="muted" style="font-size:.9rem">Mit „<?= e(authentik_config()['label']) ?>“ auf der Anmeldeseite kommen Sie damit direkt in dieses Konto, auch wenn sich die E-Mail-Adresse in authentik oder DeskHub später ändert. Ein anderes authentik-Konto kann dieses DeskHub-Konto nicht öffnen.</p>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
||||
<a class="btn secondary" href="<?= e(plugin_login_url(AUTHENTIK, 'link')) ?>">Mit einem anderen authentik-Konto neu verknüpfen</a>
|
||||
<form method="post" data-confirm="Verknüpfung wirklich lösen?" onsubmit="return confirm(this.dataset.confirm)"><?= csrf_field() ?><input type="hidden" name="action" value="unlink"><button class="btn secondary" type="submit">Verknüpfung lösen</button></form>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p><strong style="color:#b45309">Noch nicht verknüpft.</strong> Verknüpfen Sie Ihr DeskHub-Konto einmal mit Ihrem authentik-Konto. Sie melden sich dazu kurz bei authentik an und kommen hierher zurück.</p>
|
||||
<p class="muted" style="font-size:.9rem">Danach führt „<?= e(authentik_config()['label']) ?>“ auf der Anmeldeseite genau in dieses Konto. <?= $onlyLinked ? 'Ohne Verknüpfung ist die Anmeldung mit authentik hier nicht möglich.' : 'Auch ohne Verknüpfung klappt die Anmeldung, wenn die E-Mail-Adresse in authentik und DeskHub gleich ist; dabei wird sie dann automatisch verknüpft.' ?></p>
|
||||
<a class="btn" href="<?= e(plugin_login_url(AUTHENTIK, 'link')) ?>">Mit authentik verknüpfen</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"slug": "authentik",
|
||||
"name": "authentik-Anmeldung (OpenID Connect)",
|
||||
"version": "1.2.4",
|
||||
"description": "Mitarbeiter und Kunden melden sich mit authentik an (OpenID Connect, mit PKCE): Knopf „Mit authentik anmelden“ auf der Anmeldeseite des Mitarbeiterbereichs und, wenn eingeschaltet, des Kundenportals. Mitarbeiter: Angemeldet wird nur, wer in authentik bestätigt ist und in DeskHub ein aktives, verknüpftes Mitarbeiterkonto (oder eines mit derselben E-Mail-Adresse) hat; sie verknüpfen ihr Konto selbst unter „Mein authentik-Konto“. Kunden (Ticketersteller): Die bestätigte E-Mail-Adresse muss passen, das Konto wird bei Bedarf automatisch angelegt. Optional nur Mitglieder bestimmter authentik-Gruppen. E-Mail und Passwort bleiben als Zugang erhalten.",
|
||||
"author": "DeskHub",
|
||||
"entry": "plugin.php",
|
||||
"requires": "1.0.52",
|
||||
"permissions": [
|
||||
"ui.menu",
|
||||
"auth.login",
|
||||
"auth.customer"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// authentik-Anmeldung: Knopf auf der Anmeldeseite, Start und Rückkehr des OpenID-Connect-Ablaufs, Einstellungsseite für Administratoren.
|
||||
if (!function_exists('plugin_register_login')) {
|
||||
return; // älterer DeskHub-Server ohne Anmelde-Erweiterung (Server 1.0.51 oder neuer nötig)
|
||||
}
|
||||
require_once __DIR__ . '/lib.php';
|
||||
|
||||
/** Kleines Symbol vor der Beschriftung des Anmelde-Knopfes (icon.svg, als Daten-Adresse: braucht keine eigene Datei-Adresse). */
|
||||
function authentik_button_icon(): string
|
||||
{
|
||||
static $html = null;
|
||||
if ($html === null) {
|
||||
$svg = @file_get_contents(__DIR__ . '/icon.svg');
|
||||
$html = $svg ? '<img src="data:image/svg+xml;base64,' . base64_encode($svg) . '" alt="" width="20" height="20" style="vertical-align:-5px;margin-right:8px;border-radius:5px">' : '';
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
plugin_register_menu('authentik', 'einstellungen', null, 'admin');
|
||||
plugin_register_menu('Mein authentik-Konto', 'konto'); // für alle Mitarbeiter: das eigene Konto verknüpfen
|
||||
plugin_register_login('start', 'authentik_login_start');
|
||||
plugin_register_login('link', 'authentik_link_start', true);
|
||||
plugin_register_login('callback', 'authentik_login_callback', true); // gleiche Rückkehr-Adresse für Anmelden und Verknüpfen
|
||||
if (function_exists('plugin_register_customer_login')) { // Server 1.0.52 oder neuer
|
||||
plugin_register_customer_login('start', 'authentik_customer_start');
|
||||
plugin_add_filter('customer.login.buttons', function ($html) {
|
||||
if (!authentik_customer_ready()) return $html;
|
||||
$cfg = authentik_config();
|
||||
return $html . '<a class="btn secondary" style="display:block;text-align:center" href="' . e(plugin_customer_login_url(AUTHENTIK, 'start')) . '">' . authentik_button_icon() . e($cfg['label']) . '</a>';
|
||||
});
|
||||
}
|
||||
plugin_add_filter('staff.login.buttons', function ($html) {
|
||||
if (!authentik_ready()) return $html;
|
||||
$cfg = authentik_config();
|
||||
return $html . '<a class="btn secondary" style="display:block;text-align:center" href="' . e(plugin_login_url(AUTHENTIK, 'start')) . '">' . authentik_button_icon() . e($cfg['label']) . '</a>';
|
||||
});
|
||||
Reference in New Issue
Block a user