Noch keine Umsetzung - nur Datenmodell, Ablaeufe und Reihenfolge zum Abschaetzen. Zwei unabhaengig lieferbare Stufen: erst Magic-Link (klein und stellt danach den Notzugang bereit), dann OIDC gegen Entra ID. Festgehaltene Entscheidungen: OIDC statt SAML, ADFS indirekt ueber Entra, kein automatisches Anlegen von Konten beim SSO-Login, SSO-Pflicht pro Mandant mit Magic-Link als Notzugang fuer owner/admin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.4 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.
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.