|
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run
Privileged accounts are never linked through the settable mail attribute. Two new ways make that workable when UPN and e-mail differ: - "Link Microsoft account" on the profile screen: the signed-in user (nonce, same browser via the state cookie, same user at the callback) signs in with Microsoft once and binds that identity. Existing links can only be removed by an administrator; an object ID bound elsewhere is refused. - "Assigned Microsoft account (UPN)" per user, editable by administrators, used by sign-in and user sync; with an option to remove a link. Sign-in now finds accounts by bound object ID first, then by assigned UPN, then by e-mail, so linked users sign in whatever their addresses. Also: third-audit report (docs/security-audit.md section 7), README section on linking administrator accounts, translations, tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .github | ||
| .wordpress-org | ||
| assets | ||
| bin | ||
| docs | ||
| includes | ||
| languages | ||
| .distignore | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| CHANGELOG.md | ||
| composer.json | ||
| LICENSE | ||
| m365-login.php | ||
| phpcs.xml.dist | ||
| README.md | ||
| readme.txt | ||
| uninstall.php | ||
M365 Login
Anmeldung an WordPress mit dem Microsoft 365 / Entra ID-Konto – sicher, schlank, gestaltbar.
Inhalt
- Auf einen Blick
- Screenshots
- So funktioniert es
- Installation
- Einrichtung in Microsoft Entra ID
- Einstellungen im Backend
- Sicherheitskonzept
- Shortcode & Hooks
- Fehlerbehebung
- Entwicklung
- Einreichung bei WordPress.org
- FAQ
- Lizenz
Auf einen Blick
| 🔑 Login per Microsoft | Ein Klick auf der Anmeldeseite, Anmeldung bei Microsoft, zurück in WordPress – fertig. |
| 📧 Zuordnung über die E-Mail-Adresse | Der Login legt keine Benutzer an. Nur wer schon ein WordPress-Konto mit derselben E-Mail hat, kommt rein. |
| 🔄 Benutzer-Sync (optional) | Importiert Microsoft-365-Benutzer als WordPress-Konten – mit Standardrolle, zusätzlichen Rollen per Gruppen-Zuordnung, wählbaren Profilfeldern und Profilbild. In Microsoft 365 deaktivierte oder gelöschte Konten werden in WordPress deaktiviert oder gelöscht. |
| 🎨 Gestaltbarer Button | Text, Icon (Microsoft-Logo oder eigenes Bild), Farben, Hover-Farbe, Rahmen, Eckenradius, Position – mit Live-Vorschau und Presets. |
| 👥 Entra-Gruppen | Optional nur Mitglieder ausgewählter Gruppen zulassen und/oder Mitglieder bestimmter Gruppen ausschließen. Gruppen werden direkt im Backend gesucht und ausgewählt. |
| 🚪 Nur-Button-Modus | Passwortfelder ausblenden und Passwort-Logins sperren – mit geheimem Fallback-Link als Notausgang. |
| 🔏 Secret oder Zertifikat | Wahlweise Client Secret oder zertifikatsbasierte Authentifizierung (RFC 7523). Zertifikat mit einem Klick im Backend erzeugen, nur der öffentliche Teil geht zu Microsoft. |
| 🛡️ Sicher by default | OpenID Connect + PKCE, Signaturprüfung, Tenant-Pinning, Konto-Bindung, verschlüsseltes Secret, Security-Audit. |
| 🌍 Übersetzbar | Englische Basis, deutsche Übersetzung (du & Sie) enthalten. |
| 📦 WordPress.org-ready | readme.txt, Lizenz, Uninstall, Plugin Check in CI, Build-Script. |
Screenshots
Vorschauen, gerendert aus dem echten Plugin-Markup und -CSS in einer nachgebauten WordPress-Oberfläche.
| Login-Seite | Nur-Button-Modus |
|---|---|
![]() |
![]() |
| M365 Login – Verbindung | M365 Login – Button |
|---|---|
![]() |
![]() |
So funktioniert es
sequenceDiagram
autonumber
participant B as Browser
participant WP as WordPress<br/>(M365 Login)
participant MS as Microsoft Entra ID
participant G as Microsoft Graph<br/>(optional)
B->>WP: Klick auf „Login mit Microsoft“
WP->>WP: state, nonce, PKCE-Verifier erzeugen<br/>State-Cookie setzen (HttpOnly)
WP-->>B: Redirect zu Microsoft (code_challenge, state, nonce)
B->>MS: Anmeldung beim Microsoft-Konto
MS-->>B: Redirect zurück mit code + state
B->>WP: /m365-login/callback?code=…&state=…
WP->>WP: State einmalig einlösen, Cookie prüfen
WP->>MS: Code + code_verifier + Client Secret (Server-zu-Server)
MS-->>WP: ID-Token
WP->>MS: Signaturschlüssel (JWKS, gecacht)
WP->>WP: Signatur, Issuer, Audience, Tenant, exp, Nonce prüfen
opt Gruppen-Beschränkung aktiv
WP->>G: checkMemberGroups(oid, erlaubte Gruppen)
G-->>WP: Treffer / kein Treffer
end
WP->>WP: Benutzer per E-Mail suchen, Objekt-ID abgleichen
WP-->>B: WordPress-Session, Redirect ins Dashboard
Tokens laufen ausschließlich zwischen deinem Server und Microsoft. Der Browser sieht nur einen Autorisierungscode, der ohne den serverseitigen PKCE-Verifier und das Client Secret wertlos ist.
Installation
Variante A – manuell (empfohlen, solange das Plugin nicht im Verzeichnis ist)
git clone https://github.com/friloo/wp-m365-login.git
cd wp-m365-login
bash bin/build-zip.sh # erzeugt build/m365-login.zip
Dann in WordPress unter Plugins → Installieren → Plugin hochladen das ZIP hochladen und aktivieren.
Alternativ den Repo-Inhalt als Ordner m365-login nach wp-content/plugins/ kopieren.
Variante B – WordPress.org (nach der Freigabe): Plugins → Installieren → „M365 Login“.
Voraussetzungen: WordPress ≥ 6.0, PHP ≥ 7.4 mit OpenSSL-Erweiterung, HTTPS auf der Website (Microsoft akzeptiert
http://nur fürlocalhost).
Einrichtung in Microsoft Entra ID
Schritt für Schritt (ca. 5 Minuten)
- Redirect-URI kopieren. In WordPress den Menüpunkt M365 Login öffnen; die URI steht in der Seitenleiste
(
https://deine-seite.tld/m365-login/callback, bei einfachen Permalinkshttps://deine-seite.tld/?m365-login=callback). - App registrieren. Microsoft Entra Admin Center → App-Registrierungen → Neue Registrierung
- Name: z. B. „WordPress Login“
- Unterstützte Kontotypen: Nur Konten in diesem Organisationsverzeichnis (Single Tenant)
- Umleitungs-URI: Plattform Web, URI aus Schritt 1
- IDs übernehmen. Auf der Übersichtsseite Anwendungs-ID (Client) und Verzeichnis-ID (Mandant) kopieren → in WordPress eintragen.
- Authentifizierung wählen.
- Zertifikat (empfohlen): In WordPress Zertifikat erzeugen → .cer herunterladen → in Entra ID Zertifikate & Geheimnisse → Zertifikate → Zertifikat hochladen. Thumbprint vergleichen.
- Client Secret: Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel → den Wert (nicht die Geheimnis-ID) in WordPress eintragen. Ablaufdatum notieren.
- E-Mail-Claim aktivieren (empfohlen). Tokenkonfiguration → Optionalen Anspruch hinzufügen → ID →
email. - Speichern und mit Tenant testen prüfen, ob Microsoft erreichbar ist.
Zusätzlich für die Gruppen-Beschränkung
Damit das Backend Gruppen suchen und beim Login die Mitgliedschaft prüfen kann, braucht die App-Registrierung Anwendungsberechtigungen (nicht delegiert) für Microsoft Graph, jeweils mit Administratorzustimmung:
| Berechtigung | Wofür |
|---|---|
GroupMember.Read.All |
Gruppen im Backend suchen |
User.Read.All |
Mitgliedschaft beim Login prüfen (checkMemberGroups, inkl. verschachtelter Gruppen) |
Directory.Read.All deckt beides ab, ist aber weiter gefasst.
Ohne Graph-Berechtigungen geht es auch: Unter Tokenkonfiguration → Gruppenanspruch hinzufügen den groups-Claim
für ID-Tokens aktivieren (am besten Der Anwendung zugewiesene Gruppen oder Sicherheitsgruppen). Dann prüft das Plugin die
Mitgliedschaft direkt im Token. Gruppen-IDs lassen sich im Backend auch von Hand einfügen. Bei mehr als 200 Gruppen pro
Benutzer liefert Microsoft keinen groups-Claim mehr („Overage“); dann greift das Plugin automatisch auf Graph zurück.
Zusätzlich für den Benutzer-Sync
| Berechtigung (Anwendung, mit Administratorzustimmung) | Wofür |
|---|---|
User.Read.All |
Benutzer, Kontostatus, Profilfelder und Profilbilder lesen |
GroupMember.Read.All |
Nur nötig, wenn Sync-Gruppen oder Rollen-Zuordnungen verwendet werden |
Außerdem muss im Tab Verbindung die Tenant-GUID eingetragen sein (nicht organizations/common).
Wichtig: Jeder Benutzer, der sich per Microsoft anmelden soll, braucht in WordPress dieselbe E-Mail-Adresse wie in Microsoft 365 – von Hand angelegt oder vom Benutzer-Sync importiert.
Einstellungen im Backend
Menüpunkt M365 Login – drei Tabs (auch als Untermenüs erreichbar), ein Formular, ein Speichern-Button.
Verbindung
| Feld | Beschreibung |
|---|---|
| Verzeichnis-ID (Tenant) | GUID des Tenants (empfohlen, aktiviert Tenant-Pinning) oder organizations / common / consumers. Im Multi-Tenant-Modus wird nur der UPN zur Zuordnung verwendet (siehe Audit H-1). |
| Anwendungs-ID (Client) | GUID der App-Registrierung. |
| Authentifizierung | Client Secret oder Zertifikat (empfohlen). Für beide Wege gibt es im Backend eine Schritt-für-Schritt-Anleitung. |
| Client Secret | Wird verschlüsselt gespeichert und nie wieder angezeigt. Leer lassen = behalten. |
| Zertifikat | Zertifikat erzeugen legt ein 3072-Bit-RSA-Schlüsselpaar mit selbstsigniertem Zertifikat (2 Jahre) an. Der private Schlüssel bleibt verschlüsselt auf dem Server; die .cer-Datei wird in Entra ID unter Zertifikate & Geheimnisse → Zertifikate hochgeladen. Alternativ eigenes PEM-Paar einfügen. |
| Kontoauswahl | select_account (Standard), none (bestehende Microsoft-Sitzung nutzen) oder login (immer Anmeldedaten verlangen). |
| Tenant testen | Lädt die OpenID-Konfiguration des Tenants – prüft ID und ausgehende Verbindung. |
Button
| Option | Beschreibung |
|---|---|
| Button-Text | Standard „Sign in with Microsoft“ / „Login mit Microsoft“ (max. 80 Zeichen). |
| Trennlinien-Text | Standard „or“ / „oder“; leer = keine Trennlinie. |
| Icon | Microsoft-Logo (eingebettet) oder eigenes Bild aus der Mediathek (PNG, SVG, JPG, WebP). Ein-/ausblendbar. |
| Farben | Hintergrund, Hintergrund (Hover), Text, Rahmen – mit Farbwähler. |
| Eckenradius | 0–50 px. |
| Position | Unter dem Login-Formular (Standard) oder darüber. |
| Presets | Microsoft dunkel, Microsoft hell, Azure-Blau, WordPress-Blau. |
Alles wird live in der Vorschau angezeigt, bevor du speicherst.
Sicherheit
| Option | Standard | Beschreibung |
|---|---|---|
| Konto an Microsoft-Objekt-ID binden | an | Beim ersten Login wird die oid gespeichert; danach muss sie übereinstimmen. Schützt vor Übernahme, wenn eine E-Mail-Adresse in Microsoft neu vergeben wird. |
| UPN-Fallback | an | Fehlt der email-Claim, wird der User Principal Name verwendet, sofern er eine gültige E-Mail-Adresse ist. |
| Angemeldet bleiben | aus | 14-Tage-Session statt Browser-Session. |
| Erlaubte E-Mail-Domains | leer | Kommagetrennte Liste, z. B. contoso.com, contoso.de. |
Gruppen-Beschränkung
Im Tab Sicherheit → Erlaubte Entra-Gruppen:
- Gruppenname eintippen (oder Objekt-ID einfügen) → Suchen.
- Treffer mit Hinzufügen übernehmen – sie erscheinen als Chips mit Name und ID.
- Speichern. Ab jetzt darf sich nur anmelden, wer in mindestens einer dieser Gruppen ist (verschachtelte Mitgliedschaften zählen).
Prüfreihenfolge beim Login:
- Enthält das ID-Token einen
groups-Claim → Abgleich direkt im Token. - Sonst (oder bei Overage) → Microsoft Graph
checkMemberGroups. - Schlägt beides fehl → Anmeldung abgelehnt (fail closed), Meldung „Gruppenmitgliedschaft konnte nicht geprüft werden“.
Leere Liste = keine Beschränkung.
Ausgeschlossene Entra-Gruppen (gleicher Tab, darunter): Mitglieder dieser Gruppen können sich nie per Microsoft anmelden – auch wenn sie in einer erlaubten Gruppe sind (Ausschluss hat Vorrang, verschachtelte Mitgliedschaften zählen).
- Steht eine ausgeschlossene Gruppe im
groups-Claim → sofort abgelehnt. - Sonst wird immer Microsoft Graph gefragt (
checkMemberGroups, BerechtigungUser.Read.All), denn eingroups-Claim kann in der App-Registrierung gefiltert sein und beweist nicht, dass jemand kein Mitglied ist. - Schlägt die Graph-Prüfung fehl → Anmeldung abgelehnt (fail closed).
Die Passwort-Anmeldung betrifft das nicht; wer auch die sperren will, kombiniert es mit dem Nur-Button-Modus.
Nur-Button-Modus & Fallback
Im Tab Sicherheit → Button-only mode:
- Blendet Benutzername/Passwort-Felder und den „Passwort vergessen?“-Link aus.
- Sperrt Passwort-Logins serverseitig, nicht nur per CSS – auf
wp-login.phpund in jedem eigenen Login-Formular (authenticate-Filter). - Unterschieden wird nach Zugangsdaten, nicht nach Anfrage-Typ: Das normale Passwort wird überall abgelehnt – auch über
XML-RPC und in Login-Handlern anderer Plugins, die in
xmlrpc.phpoder einer REST-Anfrage laufen. Application Passwords (REST, XML-RPC) und WP-CLI funktionieren weiter; API-Anfragen bekommen aber nie ein Login-Cookie. - Falsches und richtiges Passwort erhalten dieselbe Meldung (kein Passwort-Orakel).
- Einzelne Ausnahmen per Filter
m365_login_block_password_login. - Bleibt aktiv, auch wenn die Verbindung zu Microsoft kaputtgeht (abgelaufenes Zertifikat, rotierte Salts) – dann gibt es einen roten Hinweis im Backend, und nur der Fallback-Link oder die Konstante helfen. Passwort-Logins schalten sich nie stillschweigend wieder ein.
Fallback (Notausgang): Beim Speichern erzeugt das Plugin einen geheimen Schlüssel und zeigt den Fallback-Link an:
https://deine-seite.tld/wp-login.php?m365_fallback=AbC…xYz
Wer den Link öffnet, sieht für 30 Minuten in diesem Browser wieder das normale Formular und kann sich mit Passwort anmelden. Der Schlüssel landet nicht im Cookie (nur ein HMAC davon), Fehlversuche werden pro IP gedrosselt (10 Versuche / 15 Minuten), und über die Checkbox Neuen Schlüssel beim Speichern erzeugen lässt er sich jederzeit rotieren.
Notschalter ohne Backend-Zugang: In wp-config.php
define( 'M365_LOGIN_DISABLE_BUTTON_ONLY', true );
schaltet den Modus komplett ab. Alternativ das Plugin-Verzeichnis per FTP umbenennen.
⚠️ Vor dem Aktivieren sicherstellen, dass dein eigenes Admin-Konto per Microsoft funktioniert, und den Fallback-Link sicher ablegen.
Eigene Login-Seite
Das Plugin funktioniert auch, wenn die Anmeldung nicht über wp-login.php läuft.
| Baustein | Was passiert |
|---|---|
wp_login_form() (Themes, viele Login-Plugins) |
Der Button wird automatisch unter dem Formular eingefügt, inklusive Fehlermeldungen darüber. Im Nur-Button-Modus werden die Passwortfelder ausgeblendet. Abschaltbar im Tab Button. |
| Shortcode | [m365_login_button redirect="/dashboard/" divider="yes" messages="yes"] für Block-Editor und Page Builder (Elementor, Divi, …). |
| Template-Funktion | m365_login_button( array( 'redirect' => '/dashboard/' ) ); in Theme-Dateien, m365_login_messages(); für die Meldungen an anderer Stelle. |
| URL der Login-Seite (Tab Button → Eigene Login-Seite) | Fehlermeldungen nach einem gescheiterten Microsoft-Login, der Fallback-Link und die Weiterleitung nach dem Abmelden zeigen auf diese Seite statt auf wp-login.php. |
| Nur-Button-Modus | Greift serverseitig für alle Passwort-Logins, also auch für Formulare von Page Buildern, die das Plugin nicht ausblenden kann. |
Plugins, die wp-login.php umbenennen (z. B. WPS Hide Login), sind kompatibel, weil das Plugin durchgehend wp_login_url() verwendet.
Benutzer-Sync
Privilegierte Konten (Administratoren, Redakteure mit
unfiltered_html, alle mit Rechten an Benutzern, Plugins oder Themes – auf irgendeiner Site des Netzwerks – sowie deaktivierte Konten, die solche Rollen zurückbekämen) werden nie über das frei setzbare
Tab Benutzer-Sync. Legt WordPress-Konten für Microsoft-365-Benutzer an und hält sie aktuell – manuell per Knopfdruck, automatisch per WP-Cron (stündlich, zweimal täglich, täglich) oder per WP-CLI.
Welche Benutzer? Ohne Auswahl alle Mitglieder des Tenants; optional nur Mitglieder bestimmter Gruppen (verschachtelte Mitgliedschaften zählen). Gäste (B2B) nur auf Wunsch. Die Domain-Allowlist aus dem Tab Sicherheit gilt auch hier.
Was passiert pro Benutzer?
| Situation | Ergebnis |
|---|---|
| Kein WordPress-Konto vorhanden | Konto wird angelegt: Benutzername aus der E-Mail, Zufallspasswort, keine E-Mail an den Benutzer, Standardrolle + zugeordnete Rollen. Die Anmeldung läuft über den Microsoft-Button. |
| Konto mit derselben E-Mail existiert schon | Wird mit der Microsoft-Objekt-ID verknüpft, Profilfelder werden aktualisiert. Rollen bleiben unangetastet, außer „Auch die Rollen von Konten verwalten, die schon vor dem Sync existierten“ ist aktiv. |
| Bereits verknüpft | E-Mail-Adresse, Profilfelder, Profilbild und (bei importierten Konten) Rollen werden aktualisiert. |
| In Microsoft 365 deaktiviert | Wahlweise nichts tun, WordPress-Konto deaktivieren oder löschen. |
| In Microsoft 365 gelöscht | Wahlweise nichts tun, deaktivieren oder löschen. |
| Nicht mehr in den Sync-Gruppen | Wahlweise nichts tun, deaktivieren oder löschen. |
| Wieder aktiv in Microsoft 365 | Vom Sync deaktivierte Konten werden automatisch reaktiviert (von Hand deaktivierte nicht). |
Rollen. Jeder importierte Benutzer bekommt die Standardrolle. Darunter lassen sich Microsoft-365-Gruppen per Suche auswählen und je einer WordPress-Rolle zuordnen (z. B. „Redaktion“ → Redakteur). Zwei Modi:
- Zusätzlich zur Standardrolle – der Benutzer hat danach mehrere Rollen.
- Anstelle der Standardrolle – die erste passende Gruppe der Liste gewinnt (Reihenfolge per ↑).
Verlässt jemand eine Gruppe, wird die Rolle beim nächsten Lauf entfernt. Die Rollen importierter Konten verwaltet der Sync vollständig – manuelle Änderungen werden überschrieben.
Profilfelder. Frei wählbar: Anzeigename, Vor- und Nachname, Profilbild, Position, Abteilung, Firma, Büro, Personalnummer,
Telefon (geschäftlich/mobil), Adresse, Sprache. Namen landen in den normalen WordPress-Feldern, alles andere in User-Meta mit
dem Präfix m365_ (z. B. m365_department) und wird auf der Profilseite angezeigt. Das Profilbild wird nach
wp-content/uploads/m365-login-avatars/ geladen (Dateiname mit gesalzenem Hash statt Objekt-ID) und ersetzt überall den
Gravatar. Achtung: Avatare sind öffentlich sichtbar, wo WordPress sie anzeigt.
Microsoft 365 hat immer Vorrang, bei jedem Lauf:
| In Microsoft 365 … | … in WordPress |
|---|---|
| Feld geändert | Feld wird überschrieben. |
| Feld geleert | Feld wird geleert (m365_*-Meta gelöscht; ein leerer Anzeigename wird nicht übernommen). |
| Profilbild geändert | Neues Bild wird geladen, das alte gelöscht; die Avatar-URL ändert sich, damit Browser nicht das alte Bild zeigen. |
| Profilbild gelöscht | Bild wird gelöscht, der Avatar fällt auf Gravatar zurück. |
Die Bildversionen werden per Graph-$batch geprüft (20 Benutzer pro Anfrage); heruntergeladen werden nur geänderte Bilder,
höchstens 500 pro Lauf, der Rest folgt im nächsten (Filter m365_login_sync_photo_limit, Prüfintervall per
m365_login_sync_photo_interval). Bei einem Fehler von Microsoft bleibt das vorhandene Bild erhalten – gelöscht wird nur,
wenn Microsoft ausdrücklich „kein Foto“ meldet. Abgewählte Felder und Bilder werden beim nächsten Lauf aus den Profilen
entfernt (Vor-, Nach- und Anzeigename bleiben stehen); wird ein Benutzer in WordPress gelöscht, wird auch sein Bild gelöscht.
Deaktivierte Konten bekommen ein Zufallspasswort, verlieren ihre Rolle auf der Site (sie wird gemerkt und bei der Reaktivierung zurückgegeben) und alle Application Passwords – so bleiben sie auch gesperrt, wenn das Plugin einmal deaktiviert wird. Sie können sich überhaupt nicht mehr anmelden – weder per Microsoft noch per Passwort, Anwendungspasswort oder bestehender Session (alle Sessions werden beendet). In der Benutzerliste zeigt die Spalte Microsoft 365 den Status; per Zeilenaktion lassen sich Konten auch von Hand deaktivieren und reaktivieren.
Löschen braucht einen Benutzer, der die Beiträge übernimmt. Ohne Auswahl wird stattdessen deaktiviert – es gehen nie Inhalte verloren.
Schutzmechanismen
- Testlauf: zeigt vollständig, was angelegt, geändert, deaktiviert oder gelöscht würde – ohne etwas zu ändern.
- Sicherheitsstopp: Würde ein Lauf mehr als 20 % der verknüpften Konten (mindestens 5) deaktivieren oder löschen,
passiert gar nichts (Filter
m365_login_sync_deprovision_limit). - Fehler = Abbruch: Schlägt eine Graph-Anfrage fehl, bricht der Lauf ab, bevor irgendein Konto deaktiviert wird.
„Gelöscht“ gilt ein Konto nur, wenn Graph für genau diese Objekt-ID
404liefert. - Geschützte Konten: Administratoren, die schon vor dem Sync existierten, und das eigene Konto werden nie
deaktiviert, gelöscht oder umgestuft (Filter
m365_login_sync_protect_user). - Sperre gegen Parallelläufe, Protokoll der letzten Ausführung im Backend.
WP-CLI – empfehlenswert für große Verzeichnisse oder exakte Zeiten per System-Cron:
wp m365-login sync --dry-run # Testlauf
wp m365-login sync # echter Lauf
Administrator-Konten verknüpfen
Das mail-Attribut in Entra ID kann jeder Benutzer- oder Exchange-Administrator des Tenants frei setzen – wer es auf die
Adresse eines WordPress-Admins setzt, dürfte sonst dessen Konto übernehmen. Privilegierte Konten werden deshalb nur auf
einem dieser Wege mit einem Microsoft-Konto verknüpft (per Anmeldung oder Sync):
| Weg | Wann sinnvoll |
|---|---|
| Selbst verknüpfen: Profil → Microsoft 365 → „Mit Microsoft-Konto verknüpfen“. Die Person ist in WordPress angemeldet (beweist das WordPress-Konto) und meldet sich einmal bei Microsoft an (beweist das Microsoft-Konto). | Immer – auch wenn UPN und Mailadresse völlig verschieden sind. Im Nur-Button-Modus vorher über den Fallback-Link mit Passwort anmelden. |
| Zuweisen: Ein Administrator trägt beim Bearbeiten des Benutzers unter Microsoft 365 den UPN ein (Zugewiesenes Microsoft-Konto). Anmeldung und Sync verknüpfen genau dieses Konto. | Mehrere Admins einrichten, ohne dass jeder selbst klicken muss. |
| Automatisch: UPN eines Mitglieds (kein Gast) = WordPress-E-Mail. | Wenn UPN und Mailadresse bei euch gleich sind. |
Nach der Verknüpfung findet die Anmeldung das Konto über die unveränderliche Objekt-ID – E-Mail-Adresse oder UPN dürfen sich danach ändern. Eine bestehende Verknüpfung kann nur ein Administrator aufheben (Verknüpfung mit dem Microsoft-Konto aufheben im Profil); eine Person kann ihr Konto nicht selbst auf ein anderes Microsoft-Konto umhängen.
Sicherheitskonzept
| Bedrohung | Gegenmaßnahme |
|---|---|
| Abfangen von Tokens im Browser | Authorization Code Flow mit PKCE (S256); ID-Token wird serverseitig geholt, response_mode=query ohne Token. |
| CSRF / Login-CSRF | state ist zufällig (256 Bit), einmalig verwendbar, 10 Min. gültig und per HttpOnly-/SameSite-Cookie an den startenden Browser gebunden. |
| Token-Replay | nonce wird im ID-Token geprüft und mit dem State-Datensatz verworfen. |
| Gefälschte Tokens | Signaturprüfung gegen Microsofts JWKS (RS256 only; alg=none/HMAC werden abgelehnt), Schlüssel-Rollover wird automatisch nachgeladen. iss, aud, tid, exp, nbf, iat werden geprüft. |
| Fremde Tenants | Bei konfigurierter Tenant-GUID Tenant-Pinning; sonst Issuer-Konsistenz mit tid. |
| Kontoübernahme per E-Mail-Recycling | Bindung an die Objekt-ID (oid) beim ersten Login. |
| Unbefugte Konten | Der Login legt keine Konten an; optionale Domain-Allowlist, optionale Gruppen-Beschränkung (fail closed). Konten entstehen nur durch den explizit gestarteten bzw. aktivierten Benutzer-Sync. |
| Ausgeschiedene Mitarbeitende | Benutzer-Sync deaktiviert oder löscht Konten, die in Microsoft 365 deaktiviert/gelöscht wurden; deaktivierte Konten verlieren sofort alle Sessions und jeden Anmeldeweg. |
| Massen-Deprovisionierung durch Fehlkonfiguration | Testlauf, Sicherheitsstopp (> 20 % / min. 5), Abbruch bei jedem Graph-Fehler, „gelöscht“ nur bei 404 für die konkrete Objekt-ID, geschützte Administratoren. |
| Secret-Diebstahl aus der Datenbank | AES-256-GCM, Schlüssel per HKDF aus AUTH_KEY/SECURE_AUTH_KEY; ohne wp-config.php ist der Datensatz wertlos. Gilt für Client Secret und privaten Zertifikatsschlüssel. |
| Secret-Abfluss im Transport | Zertifikatsmodus: es wird nie ein Geheimnis übertragen, nur eine 5 Minuten gültige, signierte Client Assertion (RFC 7523). |
| Kontoübernahme im Multi-Tenant-Modus | email-Claim fremder Tenants wird ignoriert (nur UPN mit verifizierter Domain oder xms_edov). |
| Flooding der State-Tabelle | Max. 300 Login-Starts pro IP und 10 Minuten; Proxy-Header per M365_LOGIN_CLIENT_IP_HEADER – am besten ein einwertiger Header wie HTTP_CF_CONNECTING_IP oder HTTP_X_REAL_IP (bei X-Forwarded-For zählt der rechte, vom Proxy geschriebene Eintrag). |
| Offene Redirects | redirect_to läuft durch wp_validate_redirect, alle Redirects über wp_safe_redirect. |
| Fehler-Reflektion | Fehlermeldungen sind Codes → feste, übersetzte Texte; Details nur ins Log (WP_DEBUG_LOG). |
| Rate Limiting Fallback-Key | 10 Fehlversuche pro IP / 15 Min. |
Die Klassen für JWT-Prüfung, Verschlüsselung, Zertifikate und die Login-Sperre haben isolierte Tests (manipulierte Signaturen, abgelaufene Tokens, falsche Audience/Tenant/Issuer, alg=none, fremde Schlüssel, gefälschte Fallback-Cookies, schwache RSA-Schlüssel).
Der vollständige Bericht mit Bedrohungsmodell, Befunden und Betriebsempfehlungen: docs/security-audit.md.
Shortcode & Hooks
Shortcode für eigene Login-Seiten:
[m365_login_button redirect="/mein-konto/"]
Filter & Actions
// Button z. B. nur im Intranet zeigen
add_filter( 'm365_login_show_button', function ( $show ) {
return $show && 'intranet.example.com' === $_SERVER['HTTP_HOST'];
} );
// domain_hint mitschicken, damit Microsoft direkt die Firmenanmeldung zeigt
add_filter( 'm365_login_authorize_params', function ( $params ) {
$params['domain_hint'] = 'contoso.com';
return $params;
} );
// E-Mail vor dem Lookup umschreiben (z. B. Alias-Domain)
add_filter( 'm365_login_match_email', function ( $email, $claims ) {
return str_replace( '@alt.contoso.com', '@contoso.com', $email );
}, 10, 2 );
// Eigene Zusatzprüfung nach allen Plugin-Checks
add_filter( 'm365_login_allow_user', function ( $allowed, WP_User $user, array $claims ) {
return $allowed && ! in_array( 'subscriber', $user->roles, true );
}, 10, 3 );
// Nach erfolgreichem Login, z. B. Anzeigenamen synchronisieren
add_action( 'm365_login_success', function ( WP_User $user, array $claims ) {
if ( ! empty( $claims['name'] ) ) {
wp_update_user( array( 'ID' => $user->ID, 'display_name' => $claims['name'] ) );
}
}, 10, 2 );
// Ein vertrauenswürdiges Plugin darf trotz Nur-Button-Modus per Passwort anmelden
add_filter( 'm365_login_block_password_login', function ( $block, WP_User $user ) {
return doing_action( 'my_membership_plugin_login' ) ? false : $block;
}, 10, 2 );
// Redirect-URI anpassen (z. B. hinter einem Reverse Proxy)
add_filter( 'm365_login_redirect_uri', fn( $uri ) => 'https://www.example.com/m365-login/callback' );
Benutzer-Sync
// Weiteres Graph-Attribut anbieten (landet in User-Meta "m365_cost_center")
add_filter( 'm365_login_sync_attributes', function ( $attributes ) {
$attributes['costCenter'] = array( 'label' => 'Kostenstelle', 'target' => 'm365_cost_center' );
return $attributes;
} );
// Rollen pro Person anpassen (erste Rolle = Hauptrolle)
add_filter( 'm365_login_sync_roles', function ( array $roles, $oid ) {
return $roles;
}, 10, 2 );
// Daten für neu angelegte Konten (Argumente für wp_insert_user)
add_filter( 'm365_login_sync_new_user_data', function ( array $data, array $person ) {
$data['user_login'] = strtolower( $person['userPrincipalName'] );
return $data;
}, 10, 2 );
// Weitere Konten vom Sync ausnehmen
add_filter( 'm365_login_sync_protect_user', function ( $protected, WP_User $user ) {
return $protected || in_array( 'shop_manager', $user->roles, true );
}, 10, 2 );
// Sicherheitsstopp anheben (Standard: 20 % der verknüpften Konten, mindestens 5)
add_filter( 'm365_login_sync_deprovision_limit', fn( $limit, $linked ) => max( 10, $linked * 0.5 ), 10, 2 );
// Weitere: m365_login_sync_email, m365_login_sync_photo_limit, m365_login_sync_photo_interval, m365_login_is_privileged_user
// Actions: m365_login_sync_user_created, m365_login_sync_finished, m365_login_user_disabled, m365_login_user_enabled
Fehlerbehebung
| Meldung auf der Login-Seite | Ursache & Lösung |
|---|---|
| Microsoft login is not configured yet. | Tenant-ID, Client-ID oder Secret fehlt. |
| The login request expired or was invalid. | State abgelaufen (> 10 Min.), Cookie blockiert oder Seite doppelt geladen. Erneut versuchen; Cookies für die Domain erlauben. |
| Could not complete the sign-in with Microsoft. | Token-Tausch fehlgeschlagen – meist falsches/abgelaufenes Client Secret oder Redirect-URI stimmt nicht exakt mit Entra überein. Details im Log. |
| The Microsoft sign-in could not be verified. | ID-Token abgelehnt (Tenant, Audience, Signatur). Tenant-ID prüfen; Serverzeit prüfen (NTP). |
| Your Microsoft account did not provide an e-mail address. | email-Claim fehlt und UPN-Fallback ist aus oder UPN ist keine E-Mail. Claim in der Tokenkonfiguration hinzufügen. |
| No WordPress account exists for your Microsoft e-mail address. | E-Mail in WordPress stimmt nicht mit Microsoft überein. |
| This WordPress account is linked to a different Microsoft account. | Objekt-ID weicht ab. Wenn gewollt (neues Microsoft-Konto): User-Meta _m365_login_oid beim Benutzer löschen. |
| … not a member of a group that is allowed … | Benutzer ist in keiner der ausgewählten Gruppen. |
| Your group membership could not be verified. | Graph nicht erreichbar oder Berechtigung fehlt (User.Read.All) – oder groups-Claim aktivieren. Bei ausgeschlossenen Gruppen ist Graph immer nötig. |
| … member of a group that is not allowed to sign in here. | Benutzer ist Mitglied einer ausgeschlossenen Gruppe (auch verschachtelt). |
| This account has been deactivated. | Das Konto wurde vom Benutzer-Sync oder von Hand deaktiviert. Benutzer → Zeilenaktion „Reaktivieren“ – ist die Person in Microsoft 365 noch deaktiviert, deaktiviert der nächste Sync sie wieder. |
| Meldung im Sync-Protokoll | Ursache & Lösung |
|---|---|
| Microsoft Graph refused the request … | Anwendungsberechtigung User.Read.All (und bei Gruppen GroupMember.Read.All) fehlt oder keine Administratorzustimmung. |
| The user sync needs a pinned tenant ID … | Im Tab Verbindung die Tenant-GUID statt organizations/common eintragen. |
| Safety stop: … accounts would be deactivated or deleted … | Sync-Gruppen oder Tenant prüfen, Testlauf ansehen; bei gewollter Massenänderung das Limit per Filter anheben. |
| … is protected … and was not changed. | Bestehender Administrator oder eigenes Konto – bewusst ausgenommen. |
| Zeitüberschreitung beim Klick auf Jetzt synchronisieren | Der Lauf geht serverseitig weiter; Seite später neu laden. Für große Verzeichnisse wp m365-login sync verwenden. |
Logging: Mit WP_DEBUG und WP_DEBUG_LOG schreibt das Plugin Fehlerdetails mit Präfix [M365 Login] nach wp-content/debug.log. Es werden nie Tokens oder Secrets geloggt.
Entwicklung
m365-login.php Plugin-Header & Bootstrap
includes/
class-m365-login.php Verdrahtung der Komponenten
class-m365-login-settings.php Defaults, Sanitizing, Redirect-URI, Fallback-Key
class-m365-login-crypto.php AES-256-GCM für das Client Secret
class-m365-login-jwt.php RS256-Verifikation, JWKS → PEM
class-m365-login-auth.php OAuth-Flow, Callback, Benutzerzuordnung, Nur-Button-Modus
class-m365-login-graph.php Client-Credentials-Token, Paging, Benutzer/Gruppen/Fotos, checkMemberGroups
class-m365-login-sync.php Benutzer-Sync, Rollen, Profilfelder/-bilder, Deaktivierung, WP-CLI
class-m365-login-button.php Ausgabe auf wp-login.php, Shortcode
class-m365-login-admin.php Einstellungsseite, AJAX
assets/ CSS/JS für Login-Seite und Backend (unminifiziert)
languages/ .pot, de_DE, de_DE_formal
bin/ build-zip.sh, make-pot.py, compile-mo.py
docs/ Einreichungs-Checkliste
composer install # PHPCS + WordPress Coding Standards + PHPCompatibility
composer lint # php -l für alle Dateien
composer phpcs # Coding-Standards-Prüfung (phpcs.xml.dist)
python3 bin/make-pot.py # Strings extrahieren (oder: wp i18n make-pot . languages/m365-login.pot)
python3 bin/compile-mo.py # .po → .mo
bash bin/build-zip.sh # build/m365-login.zip
Die CI (.github/workflows/ci.yml) prüft Syntax unter PHP 7.4–8.4, führt PHPCS aus und lässt den offiziellen
WordPress Plugin Check über das Build-Verzeichnis laufen.
Einreichung bei WordPress.org
Das Plugin bringt alles mit, was das Review-Team verlangt: readme.txt mit External services-Abschnitt, GPL-Lizenz,
uninstall.php, eindeutige Präfixe, keine externen Assets, Übersetzungen, Verzeichnis-Icon. Die komplette Checkliste
(inkl. Slug-/Marken-Hinweisen und SVN-Schritten nach der Freigabe) steht in
docs/wordpress-org-einreichung.md.
FAQ
Kann ich Benutzer automatisch anlegen lassen?
Ja, mit dem Benutzer-Sync: Er importiert alle (oder ausgewählte) Microsoft-365-Benutzer vorab als WordPress-Konten. Der Login selbst legt weiterhin nie Konten an – wer nicht importiert oder von Hand angelegt wurde, kommt nicht rein.Funktioniert es mit privaten Microsoft-Konten (outlook.com)?
Ja, Tenant aufconsumers oder common stellen. Microsoft erlaubt dann keine Query-Strings in Redirect-URIs, deshalb müssen sprechende Permalinks aktiv sein (Callback ohne ?).
Multisite?
Ja. Einstellungen gelten pro Site, dürfen aber **nur von Super-Admins** geändert werden: Sie entscheiden, welche Microsoft-Identität sich als welcher (netzwerkweite) WordPress-Benutzer anmelden darf. Ein Site-Admin könnte sonst einen eigenen Tenant eintragen und sich als Super-Admin anmelden. Der Benutzer muss Mitglied der Site (oder Super-Admin) sein.Was passiert beim Deinstallieren?
Einstellungen, Caches (Transients), Sync-Protokoll, Cron-Termin, gespeicherte Profilbilder und die pro Benutzer gespeicherten Plugin-Daten (Objekt-ID, Deaktivierungs-Status) werden entfernt – auch in Multisite. Importierte Konten und übernommene Profilfelder (m365_*) bleiben erhalten. Deaktivierte Konten bleiben ohne Rolle, mit Zufallspasswort und ohne Application Passwords – der Deaktivierungs-Vermerk selbst wird entfernt.
Benutzer-Sync und Multisite?
Der Sync arbeitet pro Site: Neue Konten werden zur aktuellen Site hinzugefügt, „Löschen“ entfernt das Konto nur aus dieser Site. Die Deaktivierung gilt netzwerkweit, weil sie am Benutzer hängt.Ich habe mich ausgesperrt.
Fallback-Link öffnen. Kein Link zur Hand?define( 'M365_LOGIN_DISABLE_BUTTON_ONLY', true ); in die wp-config.php oder den Plugin-Ordner per FTP umbenennen.
Lizenz
GPL-2.0-or-later – siehe LICENSE. „Microsoft“, „Microsoft 365“ und das Microsoft-Logo sind Marken der Microsoft Corporation; das Plugin ist ein unabhängiges Community-Projekt und steht in keiner Verbindung zu Microsoft.




