Ergaenzt den Abschnitt, was eine direkte SAML-Anbindung ans ADFS zusaetzlich kosten wuerde - als Entscheidungsgrundlage neben dem beschlossenen OIDC-Weg, nicht als beschlossener Weg. Kernpunkte: SAML braucht ext-dom, ext-simplexml und ext-mbstring sowie Composer; in der aktuellen Dev-Umgebung fehlt jede XML-Faehigkeit (nicht einmal DOMDocument existiert), SAML liesse sich dort weder bauen noch testen. Dazu eigenes SP-Zertifikat samt Erneuerung, eine Tabelle gegen Wiedereinspielung, eine umfangreiche Pruefliste fuer eingehende Assertions und der automatische Zertifikatswechsel von ADFS als haeufigste Stoerungsursache. Empfehlung bleibt OIDC ueber Entra; SAML waere Stufe 3 nach Magic-Link und OIDC, mit der Erweiterungspruefung auf dem Zielhost als erster Aufgabe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
16 KiB
M9 Anmeldung: Magic-Link und SSO über Entra ID
Stand: 2026-07-22 — Planung, noch nicht umgesetzt.
Ziel: Mitglieder sollen sich ohne eigenes Passwort anmelden können, und ein Mandanten-Administrator soll das selbst einrichten können. Zwei Stufen, die unabhängig voneinander lieferbar sind:
- Magic-Link — Anmeldung per Einmal-Link in der E-Mail.
- SSO über Entra ID (OpenID Connect) — Anmeldung über das Konto des Kunden; dient gleichzeitig der Anbindung eines dahinterliegenden ADFS.
Die Reihenfolge ist bewusst so gewählt: Der Magic-Link ist klein, sofort nützlich und stellt danach den Notzugang bereit, den die SSO-Pflicht braucht.
Entscheidungen (getroffen)
| Frage | Entscheidung |
|---|---|
| Protokoll | OpenID Connect gegen Entra ID; kein SAML |
| ADFS | indirekt über Entra ID, nicht direkt angebunden |
| Konten anlegen | kein automatisches Anlegen beim SSO-Login |
| SSO-Pflicht | pro Mandant einschaltbar, Passwort dann deaktiviert |
| Notzugang | Magic-Link, beschränkt auf owner/admin |
| Entra-Modell | Kunde registriert eigene App in seinem Verzeichnis |
Warum kein automatisches Anlegen
Beim SSO-Login wird ausschließlich ein bestehendes Mitglied dieses
Mandanten angemeldet. Gibt es keines, erscheint ein Hinweis. Das passt zum
vorhandenen Ablauf (Mitglied anlegen → „Zugang gewähren" über
saas_grant_participant_access()) und macht eine DNS-Domain-Verifikation für
die Sicherheit entbehrlich: Ohne Anlegen und ohne Verschmelzen fremder Konten
kann ein Mandant über sein IdP keine Identitäten beanspruchen, die ihm nicht
gehören.
tenant_domains bleibt damit vorerst ungenutzt. Eine Verifikation wäre später
nur Komfort (Mandant automatisch an der E-Mail-Domain erkennen).
Was schon vorhanden ist
Beide Stufen setzen fast vollständig auf Bestehendem auf:
app/saas-auth.php saas_create_auth_token(), saas_fetch_valid_auth_token(),
saas_session_login(), saas_grant_participant_access()
app/saas-mail.php saas_send_mail(), saas_log_outbound_email(), saas_app_url()
app/rate-limit.php app_rate_limit_check()
app/audit.php app_audit_log()
user_auth_tokens user_id, tenant_id (nullable), token_type, token_hash,
expires_at, consumed_at
users password_hash ist NULL-fähig -> Konten ohne Passwort
sind bereits darstellbar
saas_create_auth_token()/saas_fetch_valid_auth_token() sind über
token_type generisch. Der Magic-Link ist damit ein neuer Token-Typ, keine
neue Infrastruktur. passwort-vergessen.php dient als Vorlage für den Ablauf
inklusive Rate-Limits.
Stufe 1: Magic-Link
Datenmodell
Keine neue Tabelle. Ergänzungen an tenant_settings:
login_password_enabled TINYINT(1) NOT NULL DEFAULT 1
login_magic_link_enabled TINYINT(1) NOT NULL DEFAULT 0
magic_link_ttl_minutes INT NOT NULL DEFAULT 15
Neuer Wert für user_auth_tokens.token_type: magic_link. Anders als beim
Passwort-Reset wird tenant_id gesetzt — der Link meldet für genau einen
Mandanten an.
Ablauf
- Auf
login.phpneben dem Passwortfeld: „Link per E-Mail anfordern" (nur sichtbar, wenn für den Mandanten aktiviert). - Eingabe E-Mail + Kundenkürzel. Rate-Limit analog
passwort-vergessen.php(pro E-Mail und pro IP). - Nutzer mit aktiver Mitgliedschaft in diesem Mandanten suchen. Antwort ist immer gleich, unabhängig davon, ob es ihn gibt.
- Token erzeugen (TTL aus den Einstellungen), Link per Mail, Versand über
saas_log_outbound_email()protokollieren. - Neue Seite
magic-login.php: Token prüfen, als verbraucht markieren,saas_session_login()mit festem Mandanten aufrufen,app_audit_log().
Zu beachten
- Token ist einmal gültig (
consumed_at) und kurzlebig (15 Minuten). - Bestehende, noch offene Magic-Link-Token des Nutzers beim Neuanfordern entwerten — sonst sammeln sich mehrere gültige Links an.
- Der Link darf nicht über den
pending-Weg der Mandantenauswahl laufen, sondern direkt in den Mandanten, für den er ausgestellt wurde. - Link nur per
POST-Bestätigung einlösen oder mitReferrer-Policyschützen, damit er nicht über Referer/Prefetch abfließt.
Aufwand
Klein — eine Migration, zwei Seiten, eine Mailvorlage, Erweiterung von
login.php und den Mandant-Einstellungen.
Stufe 2: SSO über Entra ID (OIDC)
Datenmodell
Neue Tabelle tenant_sso_providers (eine Konfiguration je Mandant):
id INT PK
tenant_id INT NOT NULL UNIQUE -> tenants(id) ON DELETE CASCADE
provider VARCHAR(30) 'entra'
directory_id VARCHAR(100) Entra-Verzeichnis (tid), hart geprüft
client_id VARCHAR(200)
client_secret_enc VARBINARY mit libsodium verschlüsselt
discovery_url VARCHAR(500)
enabled TINYINT(1)
enforce_sso TINYINT(1) Passwort/Magic-Link für Mitglieder aus
created_at/updated_at DATETIME
Ergänzung an users für die stabile Zuordnung:
sso_subject VARCHAR(200) NULL -- Entra: tid|oid
UNIQUE KEY uq_users_sso_subject (sso_subject)
Die E-Mail dient nur der ersten Zuordnung zum vorhandenen Mitglied; danach
wird über sso_subject erkannt. email/preferred_username sind in Entra
veränderbar und taugen nicht als Schlüssel.
Ablauf (Authorization Code + PKCE)
sso-start.php?tenant=<slug>: Konfiguration laden,state,nonceundcode_verifierin der Session ablegen, zu Entra weiterleiten.sso-callback.php:stateprüfen, Code gegen Token tauschen (https-Stream-Wrapper, kein curl nötig).- ID-Token prüfen: Signatur über JWKS (RS256,
openssl), dazuiss,aud,exp/iat,nonce— undtidhart gegendirectory_id. - Nutzer über
sso_subjectsuchen, sonst einmalig über die E-Mail einem vorhandenen Mitglied dieses Mandanten zuordnen undsso_subjectfestschreiben. - Kein Treffer → Hinweisseite: „Für diese E-Mail-Adresse ist in diesem Mandanten kein Zugang hinterlegt." Kein Konto anlegen.
- Treffer →
saas_session_login()mit dem Mandanten aus Schritt 1.
Sicherheitsregeln
tidimmer prüfen. Ohne diese Prüfung könnte sich jeder Entra-Mandant der Welt anmelden — der klassische Fehler bei mandantenfähigen Entra-Apps.- Nie über die Mandantenauswahl. Der SSO-Rückkanal ruft
saas_session_login()direkt mit festem Mandanten auf. Heute ist dieser Weg ohnehin zu (mandant-auswahl.phpverlangt denpending-Zustand); er darf nicht nachträglich geöffnet werden. - Client-Secret verschlüsselt ablegen (libsodium, Schlüssel aus der Env,
z. B.
APP_SECRET_KEY), niemals im Klartext und nie im Formular zurückspielen — nur „gesetzt/nicht gesetzt" anzeigen. - Ablauf des Secrets ist die wahrscheinlichste Störung (Entra-Standard: 6–24 Monate). Ablaufdatum mitpflegen und rechtzeitig warnen.
SSO-Pflicht und Notzugang
Ist enforce_sso gesetzt:
| Rolle | Passwort | Magic-Link | Entra |
|---|---|---|---|
member, viewer, treasurer |
aus | aus | ja |
owner, admin |
aus | ja (Notzugang) | ja |
Jede Nutzung des Notzugangs wird im Audit-Log vermerkt; zusätzlich eine Info-Mail an die übrigen Inhaber, damit eine stille Umgehung auffällt. Grund: Ein abgelaufenes Client-Secret sperrt sonst genau die Personen aus, die es reparieren müssten — der Mailversand ist davon nicht betroffen.
enforce_sso darf sich erst einschalten lassen, nachdem mindestens eine
erfolgreiche Testanmeldung über Entra stattgefunden hat. Sonst sperrt eine
fehlerhafte Konfiguration den Mandanten sofort aus.
Einrichtung durch den Mandanten-Administrator
Neuer Abschnitt in mandant-einstellungen.php:
- Anzeige der Redirect-URI zum Kopieren (
https://…/sso-callback.php). - Eingabe von Verzeichnis-ID, Client-ID, Client-Secret.
- Schaltfläche „Verbindung testen" — führt einen vollständigen Anmeldelauf aus und zeigt die zurückgelieferten Claims an.
- Erst nach erfolgreichem Test: „SSO verpflichtend" aktivierbar.
Als Anleitung für die Entra-Seite: App-Registrierung anlegen, Redirect-URI vom
Typ Web eintragen, Client-Secret erzeugen, Berechtigungen openid,
profile, email.
Aufwand
Deutlich größer als Stufe 1 — der Flow selbst ist überschaubar, aber JWKS-Abruf mit Cache, Token-Prüfung, verschlüsselte Secrets, Testfunktion und die Admin-Oberfläche summieren sich.
Reihenfolge
- Migration Login-Einstellungen + Magic-Link (Stufe 1) — danach ist der Notzugang vorhanden.
tenant_sso_providers+ Secret-Verschlüsselung.- OIDC-Flow ohne Oberfläche, per Konfiguration in der Env testbar.
- Admin-Oberfläche inklusive Testanmeldung.
enforce_ssoscharfschalten, Notzugangs-Regeln und Protokollierung.
Alternative: SAML 2.0 direkt gegen ADFS
Nur relevant, wenn ein Kunde die Anmeldung direkt am ADFS will statt über Entra ID. Hier steht, was das zusätzlich kostet — als Entscheidungsgrundlage, nicht als beschlossener Weg.
Voraussetzungen (zuerst klären)
SAML heißt XML, und XML heißt PHP-Erweiterungen:
ext-dom Pflicht
ext-simplexml Pflicht
ext-mbstring Pflicht
ext-openssl Pflicht (vorhanden)
ext-zlib für Redirect-Binding (vorhanden)
Composer für die Bibliothek
In der aktuellen Dev-Umgebung ist davon nichts vorhanden — DOMDocument
existiert nicht einmal. SAML lässt sich hier weder entwickeln noch testen. Vor
allem anderen muss deshalb geklärt werden:
- Welche Erweiterungen bringt der Netcup-Webspace mit (
php -mdort)? - Wie sieht eine Entwicklungsumgebung aus, in der man das überhaupt bauen kann?
Ist Punkt 1 negativ, endet der SAML-Weg hier.
Bibliothek
onelogin/php-saml (v4) ist der De-facto-Standard und die realistische Wahl.
SimpleSAMLphp ist ein vollständiges Föderations-Framework und für einen
einzelnen Service Provider deutlich überdimensioniert.
Selbst implementieren ist ausgeschlossen. XML-Signaturprüfung ist der klassische Ort für XML Signature Wrapping: Der Angreifer hängt eine zweite, manipulierte Assertion so ins Dokument, dass die Signaturprüfung das echte Element prüft, die Anwendung aber das gefälschte liest. Diese Klasse von Fehlern hat über Jahre reihenweise SAML-Implementierungen getroffen.
Datenmodell
tenant_sso_providers um SAML-Felder erweitern (oder eigene Tabelle):
protocol VARCHAR(10) 'oidc' | 'saml'
idp_entity_id VARCHAR(500)
idp_sso_url VARCHAR(500)
idp_slo_url VARCHAR(500) optional
idp_x509_certs TEXT mehrere! siehe Zertifikatswechsel
idp_metadata_url VARCHAR(500) für automatische Aktualisierung
sp_entity_id VARCHAR(500)
attr_email VARCHAR(200) welches Attribut die E-Mail trägt
Zusätzlich eine Tabelle gegen Wiedereinspielung:
saml_seen_assertions assertion_id (UNIQUE), tenant_id, expires_at
Ohne diese Tabelle kann eine einmal abgefangene Assertion innerhalb ihres Gültigkeitsfensters mehrfach eingelöst werden.
Endpunkte
Alle pfadbasiert auf APP_HOST, keine Subdomain je Mandant:
/saml/metadata.php?tenant=<slug> SP-Metadaten (XML) für den Kunden
/saml/acs.php?tenant=<slug> Assertion Consumer Service (HTTP-POST)
/saml/sls.php?tenant=<slug> Single Logout (optional)
SP-Zertifikat
Der Service Provider braucht ein eigenes Schlüsselpaar (selbstsigniert genügt), dessen öffentlicher Teil in den Metadaten steht:
- Privater Schlüssel außerhalb des Webroots, Rechte 0600.
- Laufzeit und Erneuerung einplanen — läuft es ab, bricht die Anmeldung.
- Bei Erneuerung müssen die Kunden die Metadaten neu importieren, sofern sie sie nicht per URL automatisch beziehen.
Prüfliste für eingehende Assertions
Diese Punkte muss die Umsetzung nachweislich abdecken:
- Die Assertion selbst muss signiert sein, nicht nur die Response.
Issuerstimmt mitidp_entity_iddes Mandanten überein.Audienceist unseresp_entity_id.Destination/Recipiententsprechen unserer ACS-URL.NotBefore/NotOnOrAftergültig, mit definierter Toleranz.InResponseTopasst zu einer von uns gestellten Anfrage.assertion_idwurde noch nicht verwendet (Wiedereinspielung).- Unsignierte Assertions und SHA-1 werden abgelehnt.
Zur Zeittoleranz: SAML reagiert empfindlich auf Uhrabweichungen. Die
Angleichung von PHP und MySQL (siehe Commit 064c872) ist dafür Voraussetzung;
zusätzlich sollte die Serverzeit per NTP laufen.
ADFS-Besonderheiten
- Automatischer Zertifikatswechsel: ADFS erneuert sein Token-Signing- Zertifikat standardmäßig selbsttätig (jährlich, Ankündigung 30 Tage vorher). Wer nur ein Zertifikat fest hinterlegt, steht an diesem Tag still. Deshalb entweder mehrere Zertifikate parallel akzeptieren oder die IdP-Metadaten regelmäßig per Cron neu einlesen. Das ist die mit Abstand häufigste Störungsursache bei ADFS-Anbindungen.
- Claim Rules muss der Kunde bei sich anlegen: E-Mail bzw. UPN als NameID oder als Attribut. Ohne passende Regel kommt eine Assertion ohne E-Mail an.
- ADFS erwartet SHA-256; SHA-1 ist abgekündigt.
- Der Kunde importiert unsere Metadaten als Relying Party Trust (per URL oder Datei).
Mandantenfähigkeit
Ein Service Provider, viele IdPs. Der Mandant ergibt sich aus dem
aufgerufenen ACS-Pfad und muss zusätzlich gegen den Issuer der Assertion
geprüft werden. Stimmen beide nicht überein, wird abgelehnt — sonst könnte
eine gültige Assertion aus Mandant A an der ACS-URL von Mandant B eingereicht
werden.
Es gelten unverändert die Regeln aus dem OIDC-Teil: kein automatisches
Anlegen von Konten, saas_session_login() direkt mit festem Mandanten, nie
über die Mandantenauswahl.
Oberfläche für den Mandanten-Administrator
- SP-Metadaten zum Herunterladen bzw. als URL zum Kopieren.
- IdP-Metadaten hinterlegen: als URL (bevorzugt, ermöglicht automatische Aktualisierung) oder als hochgeladene XML-Datei.
- Beim Einlesen fremder XML-Dateien externe Entitäten und Netzwerkzugriffe unterbinden (XXE); unter PHP 8 ist das Standard, muss aber abgesichert bleiben.
- Testanmeldung, bevor
enforce_ssoaktiviert werden darf.
Testbarkeit
Ohne echtes ADFS zum Ausprobieren wird das nichts. Möglichkeiten, absteigend nach Aussagekraft: ein ADFS-Testsystem des Kunden, ein lokales SimpleSAMLphp als Test-IdP, oder Entra ID als SAML-IdP konfiguriert (der Kunde hat es ohnehin) — Letzteres testet den Weg allerdings nicht gegen ADFS selbst.
Aufwand im Vergleich
| OIDC (Entra) | SAML (ADFS) | |
|---|---|---|
| Zusätzliche PHP-Erweiterungen | keine | dom, simplexml, mbstring |
| Composer nötig | nein | ja |
| Fremdbibliothek | optional | zwingend |
| Eigene Zertifikate | nein | ja, inkl. Erneuerung |
| Zertifikatswechsel der Gegenseite | entfällt | jährlich, bricht sonst |
| Wiedereinspielungsschutz | über nonce |
eigene Tabelle nötig |
| Prüfschritte am Token | überschaubar | umfangreiche Prüfliste |
| In der Dev-Umgebung baubar | ja | nein |
Empfehlung
Weiterhin OIDC über Entra ID. SAML lohnt sich nur, wenn ein Kunde ausdrücklich kein Entra für diese Anwendung zulässt — und selbst dann wäre der Umweg über einen Broker (Keycloak/Authentik übersetzt SAML nach OIDC) meist günstiger als eine eigene SAML-Implementierung samt Zertifikatspflege.
Falls SAML doch kommt: nach Stufe 1 und 2, als eigenständige Stufe 3, und mit der Erweiterungsprüfung auf dem Zielhost als erster Aufgabe.
Offene Punkte
- Composer: Für OIDC nicht zwingend nötig (
openssl+json+https-Wrapper genügen). Falls doch eine JWT-Bibliothek gewünscht ist, müsste Composer auf dem Netcup-Webspace eingerichtet werden. Welche PHP-Erweiterungen dort verfügbar sind, ist noch zu prüfen (php -m). - Mehrere IdPs je Mandant ist bewusst nicht vorgesehen (eine
Konfiguration je Mandant). Bei Bedarf später
UNIQUEauftenant_idlösen. - SCIM/automatische Bereitstellung aus Entra ist bewusst nicht Teil des Plans — durch die Entscheidung gegen automatisches Anlegen bleibt die Mitgliederpflege in der Kaffeeliste.