# M365 Login **Anmeldung an WordPress mit dem Microsoft 365 / Entra ID-Konto – sicher, schlank, gestaltbar.** [![WordPress](https://img.shields.io/badge/WordPress-6.0%2B-21759b?logo=wordpress&logoColor=white)](https://wordpress.org/) [![PHP](https://img.shields.io/badge/PHP-7.4%2B-4f5b93?logo=php&logoColor=white)](https://www.php.net/) [![License](https://img.shields.io/badge/Lizenz-GPL--2.0--or--later-blue.svg)](LICENSE) [![Plugin Check](https://img.shields.io/badge/WordPress.org-Plugin%20Check%20ready-2e7d32)](docs/wordpress-org-einreichung.md) Login-Seite mit Microsoft-Button, Farb-Presets und Sicherheitsmerkmalen
--- ## Inhalt - [Auf einen Blick](#auf-einen-blick) - [Screenshots](#screenshots) - [So funktioniert es](#so-funktioniert-es) - [Installation](#installation) - [Einrichtung in Microsoft Entra ID](#einrichtung-in-microsoft-entra-id) - [Einstellungen im Backend](#einstellungen-im-backend) - [Verbindung](#verbindung) - [Button](#button) - [Sicherheit](#sicherheit) - [Gruppen-Beschränkung](#gruppen-beschränkung) - [Nur-Button-Modus & Fallback](#nur-button-modus--fallback) - [Eigene Login-Seite](#eigene-login-seite) - [Benutzer-Sync](#benutzer-sync) - [Sicherheitskonzept](#sicherheitskonzept) - [Shortcode & Hooks](#shortcode--hooks) - [Fehlerbehebung](#fehlerbehebung) - [Entwicklung](#entwicklung) - [Einreichung bei WordPress.org](#einreichung-bei-wordpressorg) - [FAQ](#faq) - [Lizenz](#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. 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](docs/security-audit.md). | | 🌍 **Ü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 | | --- | --- | | ![Login-Seite mit Microsoft-Button](.github/assets/screenshots/login.png) | ![Login-Seite im Nur-Button-Modus](.github/assets/screenshots/login-button-only.png) | | M365 Login – Verbindung | M365 Login – Button | | --- | --- | | ![Tab Verbindung](.github/assets/screenshots/settings-connection.png) | ![Tab Button mit Live-Vorschau](.github/assets/screenshots/settings-button.png) |
M365 Login – Sicherheit (Gruppen-Auswahl, Nur-Button-Modus, Fallback-Link) ![Tab Sicherheit](.github/assets/screenshots/settings-security.png)
--- ## So funktioniert es ```mermaid sequenceDiagram autonumber participant B as Browser participant WP as WordPress
(M365 Login) participant MS as Microsoft Entra ID participant G as Microsoft Graph
(optional) B->>WP: Klick auf „Login mit Microsoft“ WP->>WP: state, nonce, PKCE-Verifier erzeugen
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)** ```bash 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ür `localhost`). --- ## Einrichtung in Microsoft Entra ID
Schritt für Schritt (ca. 5 Minuten) 1. **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 Permalinks `https://deine-seite.tld/?m365-login=callback`). 2. **App registrieren.** [Microsoft Entra Admin Center](https://entra.microsoft.com/) → *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 3. **IDs übernehmen.** Auf der Übersichtsseite **Anwendungs-ID (Client)** und **Verzeichnis-ID (Mandant)** kopieren → in WordPress eintragen. 4. **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. 5. **E-Mail-Claim aktivieren** (empfohlen). *Tokenkonfiguration → Optionalen Anspruch hinzufügen → ID → `email`*. 6. **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](#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**: 1. Gruppenname eintippen (oder Objekt-ID einfügen) → *Suchen*. 2. Treffer mit *Hinzufügen* übernehmen – sie erscheinen als Chips mit Name und ID. 3. Speichern. Ab jetzt darf sich nur anmelden, wer in **mindestens einer** dieser Gruppen ist (verschachtelte Mitgliedschaften zählen). Prüfreihenfolge beim Login: 1. Enthält das ID-Token einen `groups`-Claim → Abgleich direkt im Token. 2. Sonst (oder bei Overage) → Microsoft Graph `checkMemberGroups`. 3. Schlägt beides fehl → **Anmeldung abgelehnt** (fail closed), Meldung „Gruppenmitgliedschaft konnte nicht geprüft werden“. Leere Liste = keine Beschränkung. ### 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.php` und in jedem eigenen Login-Formular (`authenticate`-Filter). - Application Passwords, REST API, XML-RPC, WP-CLI und Cron sind nicht betroffen. Einzelne Ausnahmen per Filter `m365_login_block_password_login`. - Wird erst aktiv, wenn die Verbindung vollständig konfiguriert ist. **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` ```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 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** 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 `404` liefert. - **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: ```bash wp m365-login sync --dry-run # Testlauf wp m365-login sync # echter Lauf ``` --- ## 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. 30 Login-Starts pro IP und 10 Minuten; Proxy-Header per `M365_LOGIN_CLIENT_IP_HEADER`. | | 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](docs/security-audit.md)**. --- ## Shortcode & Hooks **Shortcode** für eigene Login-Seiten: ``` [m365_login_button redirect="/mein-konto/"] ``` **Filter & Actions** ```php // 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** ```php // 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 // 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. | | *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 ``` ```bash 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](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 auf consumers 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; 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 sind danach wieder aktiv; wer sie sperren will, sollte sie vorher löschen.
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](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.