diff --git a/assets/js/admin.js b/assets/js/admin.js
index 322fd04..28b83f6 100644
--- a/assets/js/admin.js
+++ b/assets/js/admin.js
@@ -3,6 +3,7 @@
'use strict';
var cfg = window.m365LoginAdmin || {};
+ cfg.nonces = cfg.nonces || {};
var i18n = cfg.i18n || {};
$( function () {
@@ -166,7 +167,7 @@
$btn.prop( 'disabled', true );
$out.removeClass( 'is-success is-error' ).prop( 'hidden', false ).text( i18n.generating || '…' );
- $.post( cfg.ajaxUrl, { action: cfg.certAction, nonce: cfg.nonce, op: 'generate' } ).done( function ( res ) {
+ $.post( cfg.ajaxUrl, { action: cfg.certAction, nonce: cfg.nonces.cert, op: 'generate' } ).done( function ( res ) {
if ( res && res.success ) {
$out.addClass( 'is-success' ).text( res.data.message );
window.setTimeout( function () { window.location.reload(); }, 800 );
@@ -312,7 +313,7 @@
$.post( cfg.ajaxUrl, {
action: cfg.groupAction,
- nonce: cfg.nonce,
+ nonce: cfg.nonces.groups,
query: query
} ).done( function ( res ) {
if ( ! res || ! res.success ) {
@@ -369,7 +370,7 @@
$( '.m365-sync-run' ).prop( 'disabled', true );
$report.html( $( '' ).text( i18n.syncRunning ) );
- $.post( cfg.ajaxUrl, { action: cfg.syncAction, nonce: cfg.nonce, op: op } ).done( function ( res ) {
+ $.post( cfg.ajaxUrl, { action: cfg.syncAction, nonce: cfg.nonces.sync, op: op } ).done( function ( res ) {
if ( res && res.success ) {
$report.html( res.data.html );
} else {
@@ -400,7 +401,7 @@
$.post( cfg.ajaxUrl, {
action: cfg.action,
- nonce: cfg.nonce,
+ nonce: cfg.nonces.test,
tenant: $( '#m365-tenant' ).val()
} ).done( function ( res ) {
if ( res && res.success ) {
diff --git a/docs/security-audit.md b/docs/security-audit.md
index 50eb2c3..28ef627 100644
--- a/docs/security-audit.md
+++ b/docs/security-audit.md
@@ -1,28 +1,33 @@
# Security-Audit: M365 Login 1.1.0
-**Stand:** 22.09.2026, Nachtrag Benutzer-Sync 23.09.2026 · **Umfang:** gesamter Plugin-Code (PHP, JS, CSS), Konfiguration, Deployment-Hinweise ·
-**Methode:** manuelle Code-Review gegen OAuth 2.0 / OpenID Connect Best Current Practice (RFC 6749, RFC 7636 PKCE,
+**Stand:** 23.09.2026 (Erst-Audit 22.09.2026, vollständiges Zweit-Audit 23.09.2026) · **Umfang:** gesamter Plugin-Code
+(PHP, JS, CSS) inkl. Benutzer-Sync, Konfiguration, Deployment-Hinweise ·
+**Methode:** Code-Review gegen OAuth 2.0 / OpenID Connect Best Current Practice (RFC 6749, RFC 7636 PKCE,
RFC 7523 Client Assertions, OAuth 2.0 Security BCP), OWASP ASVS 4.0 (V2 Authentication, V3 Session, V5 Validation,
-V6 Cryptography), WordPress Plugin Handbook „Security“ sowie isolierte Tests der sicherheitskritischen Klassen.
+V6 Cryptography), WordPress Plugin Handbook „Security“ – und **dynamische Tests** in einer echten WordPress-Installation
+(7.1, SQLite, PHP 8.3): selbst signierte ID-Tokens mit eigenem JWKS gegen den echten Callback, simulierte Graph-API,
+echte HTTP-Requests (PHP-Webserver + curl) gegen `wp-login.php`, `xmlrpc.php` und die REST-API.
-> Der Audit wurde ohne laufende WordPress-Instanz durchgeführt. Alle Aussagen zum Laufzeitverhalten beruhen auf
-> Code-Lesung und den isolierten Tests (JWT-Verifikation, Verschlüsselung, Zertifikate, Eingabeverarbeitung,
-> Nur-Button-Sperre). Ein Penetrationstest gegen eine echte Installation steht aus und wird empfohlen.
-> Der Benutzer-Sync (N-7) wurde zusätzlich in einer echten WordPress-Installation (7.1, SQLite) gegen eine simulierte
-> Graph-API getestet (Import, Paging, Rollen, Profilfelder, Fotos, Deaktivierung, Löschen, Sicherheitsstopp, Graph-Fehler).
+> Das Zweit-Audit (Abschnitt 6) wurde in vier getrennten Prüfbereichen durchgeführt: (1) OIDC/JWT/Kryptografie,
+> (2) Benutzer-Sync und Graph-Client, (3) Admin-Oberfläche/XSS/CSRF, (4) Umgehung von Nur-Button-Modus, Deaktivierung
+> und Gruppenregeln. Jeder Befund wurde mit einem Proof of Concept bestätigt oder als „unbestätigt“ markiert, und jeder
+> Fix ist durch einen Regressionstest belegt. Nicht getestet: echter Entra-Tenant, echte Multisite-Installation
+> (Multisite-Befunde über Code-Analyse und eine konvertierte Testinstanz), echter Browser.
## 1. Zusammenfassung
-| Schweregrad | Gefunden | Behoben | Offen (mit Empfehlung) |
-| --- | --- | --- | --- |
-| Hoch | 1 | 1 | 0 |
-| Mittel | 3 | 3 | 0 |
-| Niedrig | 5 | 3 | 2 |
-| Hinweis | 6 | – | 6 |
+| Schweregrad | Erst-Audit | Zweit-Audit | Behoben | Akzeptiert / dokumentiert |
+| --- | --- | --- | --- | --- |
+| Kritisch | 0 | 1 (nur Multisite) | 1 | 0 |
+| Hoch | 1 | 4 | 5 | 0 |
+| Mittel | 3 | 9 | 12 | 0 |
+| Niedrig | 5 | 11 | 12 | 4 |
+| Hinweis | 6 | 11 | 4 | 13 |
-Der Login-Flow ist nach dem Audit **ohne bekannte kritische oder hohe Schwachstellen**. Der einzige Hoch-Befund
-(Account-Übernahme im Multi-Tenant-Modus über den unverifizierten `email`-Claim) wurde behoben. Die beiden offenen
-Niedrig-Befunde betreffen Betriebsumgebung und Konfiguration, nicht den Code.
+Nach beiden Audits sind **keine offenen kritischen, hohen oder mittleren Befunde** bekannt. Die schwersten Funde des
+Zweit-Audits betrafen nicht den OIDC-Kern (der hielt allen Angriffen stand), sondern die Ränder: Umgehung des
+Nur-Button-Modus über `xmlrpc.php`/REST, die Verknüpfung von Administrator-Konten über das frei setzbare
+`mail`-Attribut und Multisite-Rechte.
## 2. Bedrohungsmodell
@@ -55,7 +60,8 @@ Sicherheit bleibt die Tenant-GUID die Empfehlung.
Der Nur-Button-Modus prüfte `$GLOBALS['pagenow']`; eigene Login-Formulare (`wp_signon()` von einer Seite)
umgingen die Sperre. Jetzt greift der `authenticate`-Filter (Priorität 99, nach den Core-Handlern) für jede
-interaktive Passwort-Anmeldung; ausgenommen sind XML-RPC, REST (Application Passwords), WP-CLI und Cron, plus ein
+interaktive Passwort-Anmeldung; ausgenommen sind XML-RPC, REST (Application Passwords), WP-CLI und Cron (im Zweit-Audit als
+umgehbar erkannt und auf „nur Application Passwords und WP-CLI“ verschärft, siehe Z-3), plus ein
Opt-out-Filter für vertrauenswürdige Plugins. Tests: `password login blocked without fallback cookie`,
`forged fallback cookie rejected`, `REST requests exempt`.
@@ -180,7 +186,77 @@ gebraucht werden; die Auswahl ist bewusst Opt-in (Standard: nur Namen).
8. WordPress-Salts nicht ohne Neueingabe von Secret/Zertifikat rotieren.
9. Vor Produktivgang: Durchlauf in einer Staging-Installation inkl. der Fehlerfälle (falsche E-Mail, fremde Gruppe, abgelaufenes Secret).
-## 6. Nicht im Umfang
+## 6. Zweit-Audit 1.1.0 (23.09.2026)
+
+Status: ✅ behoben (mit Regressionstest) · 📄 akzeptiert/dokumentiert
+
+### Kritisch / Hoch
+
+| ID | Befund | Status und Fix |
+| --- | --- | --- |
+| Z-1 | **Multisite: Site-Admin → Super-Admin-Übernahme.** Einstellungen verlangten nur `manage_options`. Ein Site-Admin konnte einen eigenen Tenant eintragen, einem Benutzer dort `mail` = Adresse des Super-Admins geben und sich per Microsoft als Super-Admin anmelden; der Sync verknüpfte bzw. überschrieb netzwerkweite Konten. | ✅ Auf Multisite verlangen Einstellungsseite, `options.php` (`option_page_capability_m365_login`), alle AJAX-Aktionen und der Zertifikats-Download `manage_network_options`. |
+| Z-2 | **Kontoübernahme über das `mail`-Attribut (Sync und Login).** Ein Benutzer-/Exchange-Admin des Tenants setzt `mail` = E-Mail eines noch nicht verknüpften WordPress-Admins → Sync bzw. erste Anmeldung verknüpft das Admin-Konto mit seiner Objekt-ID; bei verknüpften Konten schrieb der Sync die WordPress-E-Mail ohne Benachrichtigung um (→ Passwort-Reset). | ✅ Privilegierte Konten (`manage_options`, `promote_users`, `edit_users`, Super-Admin) werden nur verknüpft, wenn der UPN eines Mitglieds (kein Gast, keine externe Identität) exakt ihrer E-Mail entspricht – im Sync und bei der ersten Anmeldung (`privileged_unlinked`). Ihre E-Mail ändert der Sync nie; bei anderen Konten bleibt die Änderungs-Mail an die alte Adresse aktiv. Filter `m365_login_is_privileged_user`. |
+| Z-3 | **Nur-Button-Modus über `xmlrpc.php` umgehbar.** Die Ausnahme hing am Anfragekontext (`XMLRPC_REQUEST`), nicht an der Art der Zugangsdaten. Ein Login-Formular eines anderen Plugins (z. B. WooCommerce auf `wp_loaded`), per POST an `/xmlrpc.php` geschickt, lieferte ein vollwertiges Admin-Cookie; außerdem akzeptierte XML-RPC das normale Passwort. | ✅ Ausnahme nur noch nach Zugangsdaten: Application Password (`application_password_did_authenticate`) oder WP-CLI. Normales Passwort überall abgelehnt. Zusätzlich setzt der Filter `send_auth_cookies` in XML-RPC-/REST-Anfragen nie ein Login-Cookie. Per HTTP nachgetestet. |
+| Z-4 | **Multi-Tenant: Übernahme über `preferred_username` externer Identitäten.** Gäste/föderierte Identitäten eines fremden Tenants konnten einen beliebigen Namen tragen. | ✅ Im Multi-Tenant-Modus werden Tokens mit abweichendem `idp` abgelehnt (`external_identity`); privilegierte Konten sind dort gar nicht erstmals verknüpfbar (Z-2). |
+| Z-5 | **Fehlende Log-Methode** (seit Umstellung auf eigene Login-Seiten): jeder fehlgeschlagene Microsoft-Login endete in einem PHP-Fatal-Error statt einer Meldung. | ✅ `log()` wiederhergestellt (eigener Commit). |
+
+### Mittel
+
+| ID | Befund | Status und Fix |
+| --- | --- | --- |
+| Z-6 | Application Password → interaktive Session: `POST /wp/v2/users/me {password}` setzt in Core ein Login-Cookie. | ✅ `send_auth_cookies`-Sperre in REST/XML-RPC (Z-3). |
+| Z-7 | REST-Login-Endpunkte anderer Plugins (`wp_signon` in einer Route) waren durch die `REST_REQUEST`-Ausnahme freigestellt. | ✅ Ausnahme entfernt (Z-3). |
+| Z-8 | Passwort-Orakel: falsches Passwort → „incorrect“, richtiges → „Password sign-in is disabled“. | ✅ Im Nur-Button-Modus dieselbe Meldung für jeden Passwortversuch. |
+| Z-9 | In Entra deaktivierte, verknüpfte Personen wurden nicht deaktiviert, wenn ihre Mail leer war oder auf eine nicht erlaubte Domain wechselte (typisch beim Offboarding). | ✅ Kontostatus verknüpfter Personen wird vor jeder E-Mail-/Domain-Prüfung ausgewertet. |
+| Z-10 | Deaktivierung hielt nur, solange das Plugin aktiv war (Passwort und Application Passwords blieben gültig; Deinstallation entsperrte). | ✅ Deaktivierung setzt ein Zufallspasswort, löscht alle Application Passwords, entzieht die Rolle (gemerkt, bei Reaktivierung zurück) und beendet alle Sessions. |
+| Z-11 | Multisite: Deaktivierung durch eine Unter-Site galt netzwerkweit, „Löschen“ entfernte nur aus der Site. | ✅ Durch Z-1 nur noch von Super-Admins konfigurierbar; Verhalten dokumentiert (README, FAQ). |
+| Z-12 | Private Schlüssel im Zertifikatsfeld: ein eingefügtes Key+Cert-Bündel wurde als „Zertifikat“ im Klartext gespeichert und im `.cer`-Download ausgeliefert. | ✅ Zertifikatsfeld mit `PRIVATE KEY` wird abgelehnt; gespeichert wird nur das per `openssl_x509_export` neu exportierte Einzelzertifikat. |
+| Z-13 | Nur-Button-Modus schaltete sich ab, sobald die Verbindung als „nicht konfiguriert“ galt (abgelaufenes Zertifikat, rotierte Salts) → Passwort-Login ohne MFA kam still zurück. | ✅ Modus hängt nur an Einstellung, Tenant/Client-ID und Fallback-Key. Bei kaputter Verbindung roter Admin-Hinweis und Meldung auf der Login-Seite. |
+| Z-14 | IP-Limits als globaler DoS hinter NAT/Proxy (30 Login-Starts/10 Min.; 10 falsche Fallback-Keys sperrten auch den richtigen); linker, vom Client gesetzter XFF-Eintrag. | ✅ Start-Limit 300; der richtige Fallback-Key funktioniert immer; aus dem Proxy-Header wird der rechte (vom Proxy geschriebene) Eintrag genommen. |
+
+### Niedrig
+
+| ID | Befund | Status und Fix |
+| --- | --- | --- |
+| Z-15 | Fallback-Cookie ohne serverseitiges Ablaufdatum (konstanter HMAC). | ✅ Cookie = `Zeit|HMAC(Zeit, Key)`, Alter wird serverseitig geprüft (30 Min.). |
+| Z-16 | Eine Objekt-ID an zwei Konten gebunden → nur eines wurde deprovisioniert; der Login band ohne Eindeutigkeitsprüfung. | ✅ Sync behandelt alle Konten einer Objekt-ID; der Login verweigert die Bindung einer bereits vergebenen Objekt-ID. |
+| Z-17 | Foto-Löschung prüfte den gespeicherten Pfad nur per Präfix (`../` möglich) – ausnutzbar nur mit Schreibzugriff auf Meta. | ✅ Strenge Prüfung gegen das Dateinamensmuster überall (Löschen, Avatar-URL, Vergleich). |
+| Z-18 | Sicherheitsstopp umgehbar durch leere Graph-Antwort oder Tenant-Wechsel (alle Objekt-IDs → 404 → „gelöscht“). | ✅ Leere Liste bei vorhandenen Verknüpfungen bricht ab; nach einem Tenant-Wechsel wird im ersten Lauf nichts deprovisioniert. |
+| Z-19 | Lauf-Sperre nicht atomar (Transient), lief bei langen Läufen ab. | ✅ Atomare Sperre per `add_option` mit Token, 2 h, Übernahme nur bei veralteter Sperre. |
+| Z-20 | Query-Parameter-Injektion in die Start-URL des Buttons (`add_query_arg` kodiert nicht). | ✅ `redirect_to` wird validiert und URL-kodiert. |
+| Z-21 | `sanitize()` stürzte bei Array-Eingaben ab bzw. speicherte `"Array"` als Secret (nur durch Admin manipulierbar). | ✅ Skalar-Prüfung für alle Textfelder. |
+| Z-22 | Graph-Retries mit `sleep` im interaktiven Login (bis ~30 s pro Prüfung). | ✅ Keine Retries im Login-Pfad, nur im Sync. |
+| Z-23 | Gruppenregeln im Multi-Tenant-Modus blockieren jede Anmeldung (fail closed, aber unerwartet). | ✅ Warnhinweis im Tab Sicherheit. |
+| Z-24 | Rollen können nach einer Entra-Gruppe vergeben werden, deren Mitglieder sich selbst eintragen können (öffentliche Microsoft-365-Gruppen, Teams). | 📄 Gruppensuche kennzeichnet öffentliche Microsoft-365-Gruppen; Warnhinweis bei der Rollen-Zuordnung (Sicherheitsgruppen, ideal rollenzuweisbar, verwenden). |
+| Z-25 | Bestehende Administratoren werden bei Austritt nicht automatisch deaktiviert (nur protokolliert). | 📄 Bewusst (Schutz vor Aussperren); im Protokoll als „geschützt“ gemeldet. |
+
+### Hinweise
+
+| ID | Hinweis | Status |
+| --- | --- | --- |
+| Z-26 | Ein AJAX-Nonce für alle Aktionen (auch destruktive). | ✅ Eigener Nonce pro Aktion. |
+| Z-27 | `wp_dropdown_roles()` gibt Rollennamen unescaped aus. | ✅ Eigene, escapte Optionsliste. |
+| Z-28 | Uninstall ließ den Graph-Token in einem persistenten Object Cache. | ✅ `delete_transient()` für die bekannten Schlüssel; Sperre und Tenant-Option werden entfernt. |
+| Z-29 | Verschlüsselung nutzlos, wenn die Salts nicht in `wp-config.php` stehen (dann in der Datenbank). | ✅ Doku korrigiert, Warnhinweis im Backend. |
+| Z-30 | Button-Icon darf eine externe http(s)-URL sein (Tracking/Mixed Content). | 📄 Admin-Entscheidung; Mediathek wird empfohlen. |
+| Z-31 | JWKS-`issuer` wird im Multi-Tenant-Modus nicht geprüft. | 📄 Risiko minimal (Token kommt direkt vom Token-Endpunkt über TLS). |
+| Z-32 | Gruppenregeln wirken nur bei der Microsoft-Anmeldung; bestehende Sessions (bis 14 Tage), Application Passwords laufen weiter; Deaktivierung aus Entra greift erst beim nächsten Sync. | 📄 Dokumentiert; Sync-Intervall und „Angemeldet bleiben“ entsprechend wählen. |
+| Z-33 | Plugins, die selbst `wp_set_current_user()`/`wp_set_auth_cookie()` nach eigener Passwortprüfung aufrufen, umgehen jede Login-Sperre. | 📄 Außerhalb der Kontrolle des Plugins. |
+| Z-34 | Fallback-Key steht in der URL (Webserver-/Proxy-Logs). | 📄 Link nach Nutzung neu erzeugen; Logs schützen. |
+| Z-35 | Im Single-Tenant-Modus melden sich B2B-Gäste über ihren `email`-Claim an. | 📄 Für normale Konten gewollt; privilegierte Konten sind für Gäste nicht verknüpfbar (Z-2). |
+| Z-36 | Profilbilder öffentlich, mit EXIF-Daten, auch nach Deaktivierung; Benutzernamen aus dem lokalen Teil der E-Mail. | 📄 Dokumentiert; Fotos/Felder sind Opt-in und werden beim Abwählen entfernt. |
+
+### Geprüft und ohne Befund (Auszug)
+
+- **OIDC-Kern:** State 256 Bit, einmalig, HMAC-Transient-Schlüssel, an HttpOnly/SameSite-Cookie gebunden; `error` erst nach State-Prüfung; PKCE S256; Nonce mit `hash_equals`; kein Login-CSRF, keine Code-Injection.
+- **JWT:** nur RS256 (`none`/HS256/Array-`kid` abgelehnt), `openssl_verify === 1`, `aud` exakt, `tid`-Pinning, `iss` aus `tid` gebildet, `exp`/`nbf`/`iat` mit 120 s Toleranz, JWKS-Refresh nur bei unbekanntem `kid`.
+- **Krypto:** AES-256-GCM mit frischem 96-Bit-IV und 16-Byte-Tag, HKDF-SHA256; manipulierte Tags abgelehnt. Client Assertion mit korrektem `aud`, `jti`, `exp`, `x5t`/`x5t#S256`.
+- **Gruppen:** Ausschluss fail closed, Overage/gefilterter Claim → Graph, Großschreibung normalisiert.
+- **Admin:** jede AJAX-Aktion mit Nonce + Capability; kein Stored/DOM-XSS (Graph-Gruppennamen, Sync-Attribute, Protokoll, Rollenname per jsdom-Fuzzing geprüft); keine CSS-Injection; kein SSRF (Tenant nur GUID/Alias); keine offenen Redirects (`//evil`, `/\evil`, `user@evil`, `javascript:`).
+- **Deaktivierte Konten:** Passwort, XML-RPC (Passwort und Application Password), REST, bestehende Cookies, Microsoft-Callback, Super-Admin – alle abgewiesen; keine Selbst-Reaktivierung möglich.
+- **Graph-Client:** Paging-Links auf `https://graph.microsoft.com/v1.0/` festgelegt, 1000-Seiten-Limit, jeder Fehler bricht vor Änderungen ab.
+
+## 7. Nicht im Umfang
Sicherheit der Microsoft-Seite (Entra ID, Graph), WordPress-Core, Hosting-Umgebung, andere Plugins/Themes,
Schwachstellen in PHP/OpenSSL.
diff --git a/includes/class-m365-login-admin.php b/includes/class-m365-login-admin.php
index cbfff4d..2ca6399 100644
--- a/includes/class-m365-login-admin.php
+++ b/includes/class-m365-login-admin.php
@@ -12,15 +12,18 @@ defined( 'ABSPATH' ) || exit;
*/
class M365_Login_Admin {
- const PAGE = 'm365-login';
- const GROUP = 'm365_login';
- const AJAX_TEST = 'm365_login_test_connection';
- const AJAX_GROUPS = 'm365_login_search_groups';
- const AJAX_CERT = 'm365_login_certificate';
- const AJAX_SYNC = 'm365_login_sync_run';
- const POST_CERT = 'm365_login_download_cert';
- const MENU_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+PHBhdGggZmlsbD0iYmxhY2siIGQ9Ik0yIDJoNy41djcuNUgyek0xMC41IDJIMTh2Ny41aC03LjV6TTIgMTAuNWg3LjVWMThIMnpNMTAuNSAxMC41SDE4VjE4aC03LjV6Ii8+PC9zdmc+';
- const NONCE_TEST = 'm365_login_test';
+ const PAGE = 'm365-login';
+ const GROUP = 'm365_login';
+ const AJAX_TEST = 'm365_login_test_connection';
+ const AJAX_GROUPS = 'm365_login_search_groups';
+ const AJAX_CERT = 'm365_login_certificate';
+ const AJAX_SYNC = 'm365_login_sync_run';
+ const POST_CERT = 'm365_login_download_cert';
+ const MENU_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+PHBhdGggZmlsbD0iYmxhY2siIGQ9Ik0yIDJoNy41djcuNUgyek0xMC41IDJIMTh2Ny41aC03LjV6TTIgMTAuNWg3LjVWMThIMnpNMTAuNSAxMC41SDE4VjE4aC03LjV6Ii8+PC9zdmc+';
+ const NONCE_TEST = 'm365_login_test';
+ const NONCE_GROUPS = 'm365_login_groups';
+ const NONCE_CERT = 'm365_login_cert';
+ const NONCE_SYNC = 'm365_login_sync';
/**
* Settings.
@@ -82,6 +85,19 @@ class M365_Login_Admin {
add_action( 'update_option_' . M365_LOGIN_OPTION, array( $this->graph, 'flush_token' ) );
add_action( 'admin_notices', array( $this, 'setup_notice' ) );
add_filter( 'submenu_file', array( $this, 'highlight_submenu' ) );
+ add_filter( 'option_page_capability_' . self::GROUP, array( __CLASS__, 'capability' ) );
+ }
+
+ /**
+ * Capability needed for the settings, the connection test, certificates and the user sync.
+ *
+ * On multisite only super admins: the settings decide which Microsoft identities may sign in
+ * as which (network-wide) WordPress users, so a site administrator must not control them.
+ *
+ * @return string
+ */
+ public static function capability() {
+ return is_multisite() ? 'manage_network_options' : 'manage_options';
}
/**
@@ -91,7 +107,7 @@ class M365_Login_Admin {
$this->hook = add_menu_page(
__( 'M365 Login', 'm365-login' ),
__( 'M365 Login', 'm365-login' ),
- 'manage_options',
+ self::capability(),
self::PAGE,
array( $this, 'render' ),
self::MENU_ICON,
@@ -103,7 +119,7 @@ class M365_Login_Admin {
self::PAGE,
$label . ' – ' . __( 'M365 Login', 'm365-login' ),
$label,
- 'manage_options',
+ self::capability(),
'connection' === $tab ? self::PAGE : 'admin.php?page=' . self::PAGE . '&tab=' . $tab
);
}
@@ -182,7 +198,19 @@ class M365_Login_Admin {
* Nudges administrators to finish the setup.
*/
public function setup_notice() {
- if ( $this->settings->is_configured() || ! current_user_can( 'manage_options' ) ) {
+ if ( ! current_user_can( self::capability() ) ) {
+ return;
+ }
+ if ( ! $this->settings->is_configured() && $this->settings->button_only() ) {
+ printf(
+ '
',
+ esc_html__( 'M365 Login: button-only mode is on, but the connection to Microsoft is broken (missing or undecryptable secret, or expired certificate). Nobody can sign in except through the fallback link.', 'm365-login' ),
+ esc_url( self::url() ),
+ esc_html__( 'Open the settings', 'm365-login' )
+ );
+ return;
+ }
+ if ( $this->settings->is_configured() ) {
return;
}
$screen = get_current_screen();
@@ -220,7 +248,12 @@ class M365_Login_Admin {
'm365LoginAdmin',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
- 'nonce' => wp_create_nonce( self::NONCE_TEST ),
+ 'nonces' => array(
+ 'test' => wp_create_nonce( self::NONCE_TEST ),
+ 'groups' => wp_create_nonce( self::NONCE_GROUPS ),
+ 'cert' => wp_create_nonce( self::NONCE_CERT ),
+ 'sync' => wp_create_nonce( self::NONCE_SYNC ),
+ ),
'action' => self::AJAX_TEST,
'groupAction' => self::AJAX_GROUPS,
'certAction' => self::AJAX_CERT,
@@ -257,7 +290,7 @@ class M365_Login_Admin {
*/
public function ajax_test_connection() {
check_ajax_referer( self::NONCE_TEST, 'nonce' );
- if ( ! current_user_can( 'manage_options' ) ) {
+ if ( ! current_user_can( self::capability() ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'm365-login' ) ), 403 );
}
@@ -295,8 +328,8 @@ class M365_Login_Admin {
* AJAX: search Entra groups through Microsoft Graph.
*/
public function ajax_search_groups() {
- check_ajax_referer( self::NONCE_TEST, 'nonce' );
- if ( ! current_user_can( 'manage_options' ) ) {
+ check_ajax_referer( self::NONCE_GROUPS, 'nonce' );
+ if ( ! current_user_can( self::capability() ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'm365-login' ) ), 403 );
}
if ( ! $this->settings->is_configured() ) {
@@ -321,8 +354,8 @@ class M365_Login_Admin {
* AJAX: generate a new self-signed certificate and store it (key encrypted).
*/
public function ajax_certificate() {
- check_ajax_referer( self::NONCE_TEST, 'nonce' );
- if ( ! current_user_can( 'manage_options' ) ) {
+ check_ajax_referer( self::NONCE_CERT, 'nonce' );
+ if ( ! current_user_can( self::capability() ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'm365-login' ) ), 403 );
}
$op = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : '';
@@ -354,8 +387,8 @@ class M365_Login_Admin {
* AJAX: run the user sync (dry run or live) with the saved settings.
*/
public function ajax_sync() {
- check_ajax_referer( self::NONCE_TEST, 'nonce' );
- if ( ! current_user_can( 'manage_options' ) || ! current_user_can( 'create_users' ) ) {
+ check_ajax_referer( self::NONCE_SYNC, 'nonce' );
+ if ( ! current_user_can( self::capability() ) || ! current_user_can( 'create_users' ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'm365-login' ) ), 403 );
}
$op = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : '';
@@ -486,7 +519,7 @@ class M365_Login_Admin {
→
-
+ role_options( $value['role'] ); ?>
@@ -498,7 +531,7 @@ class M365_Login_Admin {
-
+ role_options( 'editor' ); ?>
@@ -506,11 +539,30 @@ class M365_Login_Admin {
elements for all editable roles (escaped, unlike wp_dropdown_roles()).
+ *
+ * @param string $selected Selected role slug.
+ */
+ private function role_options( $selected ) {
+ if ( ! function_exists( 'get_editable_roles' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/user.php';
+ }
+ foreach ( array_reverse( get_editable_roles() ) as $role => $details ) {
+ printf(
+ '%s ',
+ esc_attr( $role ),
+ selected( $selected, $role, false ),
+ esc_html( translate_user_role( $details['name'] ) )
+ );
+ }
+ }
+
/**
* Sends the public certificate as a .cer download (never the private key).
*/
public function download_certificate() {
- if ( ! current_user_can( 'manage_options' ) ) {
+ if ( ! current_user_can( self::capability() ) ) {
wp_die( esc_html__( 'You are not allowed to do this.', 'm365-login' ), 403 );
}
check_admin_referer( self::POST_CERT );
@@ -599,13 +651,14 @@ class M365_Login_Admin {
-
+ role_options( $s['sync_default_role'] ); ?>
+
group_picker( 'sync_role_map', $this->settings->sync_role_map(), 'roles', __( 'No group mapping – everybody gets the default role.', 'm365-login' ) ); ?>
@@ -703,7 +756,7 @@ class M365_Login_Admin {
* Renders the settings screen.
*/
public function render() {
- if ( ! current_user_can( 'manage_options' ) ) {
+ if ( ! current_user_can( self::capability() ) ) {
wp_die( esc_html__( 'You are not allowed to access this page.', 'm365-login' ) );
}
@@ -806,6 +859,9 @@ class M365_Login_Admin {
+
+
+
>
@@ -1085,6 +1141,9 @@ class M365_Login_Admin {
+ settings->is_multi_tenant() ) : ?>
+
+
group_picker( 'denied_groups', $this->settings->denied_groups(), 'chips', __( 'No groups excluded.', 'm365-login' ) ); ?>
@@ -1093,7 +1152,7 @@ class M365_Login_Admin {
-
+
id="m365-button-only" />
diff --git a/includes/class-m365-login-auth.php b/includes/class-m365-login-auth.php
index 56824b9..b0997b7 100644
--- a/includes/class-m365-login-auth.php
+++ b/includes/class-m365-login-auth.php
@@ -55,6 +55,8 @@ class M365_Login_Auth {
add_action( 'init', array( $this, 'maybe_accept_fallback_key' ), 6 );
// Runs after core's username/password handlers (priority 20), which would otherwise overwrite an early WP_Error.
add_filter( 'authenticate', array( $this, 'block_password_login' ), 99, 3 );
+ // No session cookies from API contexts (XML-RPC, REST) while password sign-in is disabled.
+ add_filter( 'send_auth_cookies', array( $this, 'block_api_auth_cookies' ), 99, 4 );
// Custom login page: send people back there after logging out.
add_filter( 'logout_redirect', array( $this, 'logout_redirect' ), 10, 3 );
@@ -74,16 +76,25 @@ class M365_Login_Auth {
return true; // Nothing is hidden, the form is always available.
}
$cookie = isset( $_COOKIE[ self::FALLBACK_COOKIE ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::FALLBACK_COOKIE ] ) ) : '';
- return '' !== $cookie && hash_equals( $this->fallback_cookie_value(), $cookie );
+ $parts = explode( '|', $cookie );
+ if ( 2 !== count( $parts ) || ! ctype_digit( $parts[0] ) ) {
+ return false;
+ }
+ $issued = (int) $parts[0];
+ if ( $issued > time() + 60 || time() - $issued > self::FALLBACK_TTL ) {
+ return false; // Expired on the server side, whatever the browser keeps.
+ }
+ return hash_equals( $this->fallback_cookie_value( $issued ), $cookie );
}
/**
- * Expected fallback cookie value (HMAC of the key, so the key itself never sits in the cookie).
+ * Fallback cookie value for an issue time: "time|HMAC" (the key itself never sits in the cookie).
*
+ * @param int $issued Issue timestamp.
* @return string
*/
- private function fallback_cookie_value() {
- return hash_hmac( 'sha256', 'fallback|' . $this->settings->fallback_key(), wp_salt( 'auth' ) );
+ private function fallback_cookie_value( $issued ) {
+ return $issued . '|' . hash_hmac( 'sha256', 'fallback|' . $issued . '|' . $this->settings->fallback_key(), wp_salt( 'auth' ) );
}
/**
@@ -99,20 +110,21 @@ class M365_Login_Auth {
return;
}
- // Slow down brute force attempts on the key.
- $ip_key = 'm365_login_fb_' . md5( $this->client_ip() );
- $attempts = (int) get_transient( $ip_key );
- if ( $attempts >= 10 ) {
- $this->fail( 'fallback_locked' );
- }
-
+ // The correct key always works (a shared office IP must not lock the administrator out);
+ // wrong keys are slowed down per IP. The key has ~139 bits, the limit only reduces log noise.
+ $ip_key = 'm365_login_fb_' . md5( $this->client_ip() );
if ( ! hash_equals( $this->settings->fallback_key(), $given ) ) {
+ $attempts = (int) get_transient( $ip_key );
+ if ( $attempts >= 10 ) {
+ $this->fail( 'fallback_locked' );
+ }
set_transient( $ip_key, $attempts + 1, 15 * MINUTE_IN_SECONDS );
$this->fail( 'fallback_invalid' );
}
delete_transient( $ip_key );
- $this->send_cookie( self::FALLBACK_COOKIE, $this->fallback_cookie_value(), time() + self::FALLBACK_TTL );
+ $issued = time();
+ $this->send_cookie( self::FALLBACK_COOKIE, $this->fallback_cookie_value( $issued ), $issued + self::FALLBACK_TTL );
nocache_headers();
wp_safe_redirect( add_query_arg( 'm365_fallback', 'on', $this->settings->login_page_url() ) );
exit;
@@ -149,16 +161,12 @@ class M365_Login_Auth {
if ( '' === (string) $username && '' === (string) $password ) {
return $user; // Initial form render or cookie auth, no password attempt.
}
- // Interactive password logins only: XML-RPC, REST (application passwords), WP-CLI and cron keep working.
- if ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
- || ( defined( 'REST_REQUEST' ) && REST_REQUEST )
- || ( defined( 'WP_CLI' ) && WP_CLI )
- || wp_doing_cron() ) {
+ // Exempt by credential, not by request context: application passwords (XML-RPC, REST) and
+ // WP-CLI keep working. A normal password is refused everywhere – also in forms of other
+ // plugins that happen to run inside xmlrpc.php or a REST request.
+ if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( $user instanceof WP_User && did_action( 'application_password_did_authenticate' ) ) ) {
return $user;
}
- if ( ! $user instanceof WP_User ) {
- return $user; // Already failed for another reason; keep core's message.
- }
/**
* Allows exempting a password sign-in from button-only mode (e.g. a trusted membership plugin).
@@ -166,13 +174,36 @@ class M365_Login_Auth {
* @param bool $block Whether to block. Default true.
* @param WP_User $user Authenticated user.
*/
- if ( ! apply_filters( 'm365_login_block_password_login', true, $user ) ) {
+ if ( $user instanceof WP_User && ! apply_filters( 'm365_login_block_password_login', true, $user ) ) {
return $user;
}
+ // Same answer for right and wrong passwords: the form must not become a password oracle.
return new WP_Error( 'm365_login_button_only', __( 'Password sign-in is disabled on this site. Please use the Microsoft button.', 'm365-login' ) );
}
+ /**
+ * While button-only mode is active, API requests (XML-RPC, REST) never receive session cookies.
+ *
+ * Core can set cookies there, e.g. when an application password is used to change the account
+ * password via REST, or when another plugin's login handler runs inside xmlrpc.php.
+ *
+ * @param bool $send Whether to send the cookies.
+ * @param int $expire Expiry (unused).
+ * @param int $expiration Expiration (unused).
+ * @param int $user_id User ID (0 when cookies are cleared).
+ * @return bool
+ */
+ public function block_api_auth_cookies( $send, $expire = 0, $expiration = 0, $user_id = 0 ) {
+ if ( ! $send || ! $user_id || ! $this->settings->button_only() || $this->fallback_active() ) {
+ return $send;
+ }
+ if ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
+ return false;
+ }
+ return $send;
+ }
+
/**
* Best-effort client IP for rate limiting.
*
@@ -190,7 +221,10 @@ class M365_Login_Auth {
*/
$header = apply_filters( 'm365_login_client_ip_header', defined( 'M365_LOGIN_CLIENT_IP_HEADER' ) ? M365_LOGIN_CLIENT_IP_HEADER : '' );
if ( '' !== $header && ! empty( $_SERVER[ $header ] ) ) {
- $candidate = trim( explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) )[0] );
+ // Proxies append to the list: the right-most entry was written by the trusted proxy,
+ // entries further left are supplied by the client.
+ $hops = explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) );
+ $candidate = trim( (string) end( $hops ) );
if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
$ip = $candidate;
}
@@ -254,9 +288,10 @@ class M365_Login_Auth {
* @return string
*/
public function start_url( $redirect_to = '' ) {
- $args = array( 'action' => self::ACTION_START );
+ $args = array( 'action' => self::ACTION_START );
+ $redirect_to = '' !== (string) $redirect_to ? wp_validate_redirect( (string) $redirect_to, '' ) : '';
if ( '' !== $redirect_to ) {
- $args['redirect_to'] = $redirect_to;
+ $args['redirect_to'] = rawurlencode( $redirect_to ); // add_query_arg() does not encode values.
}
return add_query_arg( $args, wp_login_url() );
}
@@ -276,7 +311,7 @@ class M365_Login_Auth {
// Cap the number of pending login attempts one client can create (state records are stored server-side).
$throttle_key = 'm365_login_start_' . md5( $this->client_ip() );
$starts = (int) get_transient( $throttle_key );
- if ( $starts >= 30 ) {
+ if ( $starts >= 300 ) {
$this->fail( 'too_many_attempts' );
}
set_transient( $throttle_key, $starts + 1, self::STATE_TTL );
@@ -421,6 +456,11 @@ class M365_Login_Auth {
$this->fail( 'invalid_token' );
}
+ // In multi-tenant mode a guest or federated identity could present any user name.
+ if ( $this->settings->is_multi_tenant() && $this->is_external_identity( $claims ) ) {
+ $this->fail( 'external_identity' );
+ }
+
$email = $this->email_from_claims( $claims );
if ( '' === $email ) {
$this->fail( 'no_email' );
@@ -454,18 +494,41 @@ class M365_Login_Auth {
}
// Bind the account to the immutable Microsoft object ID after first login.
+ $stored = strtolower( (string) get_user_meta( $user->ID, self::META_OID, true ) );
if ( $this->settings->get( 'bind_oid' ) ) {
if ( '' === $oid || ! M365_Login_Settings::is_guid( $oid ) ) {
$this->fail( 'invalid_token' );
}
- $stored = (string) get_user_meta( $user->ID, self::META_OID, true );
if ( '' !== $stored && ! hash_equals( $stored, $oid ) ) {
$this->log( sprintf( 'Object ID mismatch for user #%d.', $user->ID ) );
$this->fail( 'oid_mismatch' );
}
- if ( '' === $stored ) {
- update_user_meta( $user->ID, self::META_OID, $oid );
+ }
+
+ // Privileged accounts that are not bound yet: only the user principal name of a member
+ // account may claim them (its domain is verified in the tenant, the e-mail attribute is not).
+ if ( '' === $stored && M365_Login_Sync::is_privileged( $user ) && ! $this->may_claim_privileged( $claims, $user ) ) {
+ $this->log( sprintf( 'Refused first sign-in of privileged user #%d without a matching user principal name.', $user->ID ) );
+ $this->fail( 'privileged_unlinked' );
+ }
+
+ if ( $this->settings->get( 'bind_oid' ) && '' === $stored ) {
+ // One Microsoft identity, one WordPress account.
+ $taken = get_users(
+ array(
+ 'meta_key' => self::META_OID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_value' => $oid, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+ 'exclude' => array( $user->ID ),
+ 'fields' => 'ID',
+ 'number' => 1,
+ 'blog_id' => 0,
+ )
+ );
+ if ( ! empty( $taken ) ) {
+ $this->log( sprintf( 'Object ID is already bound to another account (user #%d).', $user->ID ) );
+ $this->fail( 'oid_mismatch' );
}
+ update_user_meta( $user->ID, self::META_OID, $oid );
}
/**
@@ -642,6 +705,35 @@ class M365_Login_Auth {
return $body;
}
+ /**
+ * Whether the token comes from a guest or another external identity provider.
+ *
+ * Entra only adds the "idp" claim when the identity provider differs from the issuer.
+ *
+ * @param array $claims Verified claims.
+ * @return bool
+ */
+ private function is_external_identity( $claims ) {
+ $idp = isset( $claims['idp'] ) && is_string( $claims['idp'] ) ? $claims['idp'] : '';
+ $iss = isset( $claims['iss'] ) && is_string( $claims['iss'] ) ? $claims['iss'] : '';
+ return '' !== $idp && $idp !== $iss;
+ }
+
+ /**
+ * Whether a token may claim a privileged account that is not bound yet.
+ *
+ * @param array $claims Verified claims.
+ * @param WP_User $user Matched account.
+ * @return bool
+ */
+ private function may_claim_privileged( $claims, $user ) {
+ $upn = ! empty( $claims['preferred_username'] ) && is_string( $claims['preferred_username'] ) ? strtolower( trim( $claims['preferred_username'] ) ) : '';
+ return '' !== $upn
+ && ! $this->settings->is_multi_tenant()
+ && ! $this->is_external_identity( $claims )
+ && hash_equals( strtolower( $user->user_email ), $upn );
+ }
+
/**
* Applies the Entra group rules: members of an excluded group are refused,
* everybody else needs membership in one of the allowed groups (if any are set).
@@ -699,7 +791,7 @@ class M365_Login_Auth {
return 'invalid_token';
}
- $matches = $this->graph->check_member_groups( $oid, $denied );
+ $matches = $this->graph->check_member_groups( $oid, $denied, false );
if ( is_wp_error( $matches ) ) {
$this->log( 'Excluded-group check via Microsoft Graph failed: ' . $matches->get_error_message() );
return 'group_check_failed';
@@ -741,7 +833,7 @@ class M365_Login_Auth {
return 'invalid_token';
}
- $matches = $this->graph->check_member_groups( $oid, $allowed );
+ $matches = $this->graph->check_member_groups( $oid, $allowed, false );
if ( is_wp_error( $matches ) ) {
$this->log( 'Group check via Microsoft Graph failed: ' . $matches->get_error_message() );
return 'group_check_failed';
@@ -899,6 +991,13 @@ class M365_Login_Auth {
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$out = array();
+ if ( $this->settings->button_only() && ! $this->settings->is_configured() && ! $this->fallback_active() ) {
+ $out[] = array(
+ 'type' => 'error',
+ 'code' => 'unavailable',
+ 'text' => __( 'Microsoft sign-in is temporarily unavailable. Please contact an administrator.', 'm365-login' ),
+ );
+ }
if ( $fallback_on && $this->settings->button_only() && $this->fallback_active() ) {
$out[] = array(
'type' => 'message',
@@ -924,24 +1023,26 @@ class M365_Login_Auth {
*/
private function error_messages() {
return array(
- 'not_configured' => __( 'Microsoft login is not configured yet.', 'm365-login' ),
- 'invalid_state' => __( 'The login request expired or was invalid. Please try again.', 'm365-login' ),
- 'access_denied' => __( 'Microsoft sign-in was cancelled.', 'm365-login' ),
- 'provider_error' => __( 'Microsoft returned an error. Please try again.', 'm365-login' ),
- 'token_exchange' => __( 'Could not complete the sign-in with Microsoft. Please try again or contact an administrator.', 'm365-login' ),
- 'invalid_token' => __( 'The Microsoft sign-in could not be verified.', 'm365-login' ),
- 'no_email' => __( 'Your Microsoft account did not provide an e-mail address.', 'm365-login' ),
- 'domain_not_allowed' => __( 'Your e-mail domain is not allowed to sign in here.', 'm365-login' ),
- 'no_user' => __( 'No WordPress account exists for your Microsoft e-mail address.', 'm365-login' ),
- 'oid_mismatch' => __( 'This WordPress account is linked to a different Microsoft account. Please contact an administrator.', 'm365-login' ),
- 'not_allowed' => __( 'You are not allowed to sign in with this account.', 'm365-login' ),
- 'not_in_group' => __( 'Your Microsoft account is not a member of a group that is allowed to sign in here.', 'm365-login' ),
- 'group_check_failed' => __( 'Your group membership could not be verified. Please contact an administrator.', 'm365-login' ),
- 'in_denied_group' => __( 'Your Microsoft account is a member of a group that is not allowed to sign in here.', 'm365-login' ),
- 'fallback_invalid' => __( 'The fallback key is not valid.', 'm365-login' ),
- 'fallback_locked' => __( 'Too many attempts. Please wait 15 minutes.', 'm365-login' ),
- 'too_many_attempts' => __( 'Too many sign-in attempts from your connection. Please wait a few minutes and try again.', 'm365-login' ),
- 'account_disabled' => __( 'This account has been deactivated.', 'm365-login' ),
+ 'not_configured' => __( 'Microsoft login is not configured yet.', 'm365-login' ),
+ 'invalid_state' => __( 'The login request expired or was invalid. Please try again.', 'm365-login' ),
+ 'access_denied' => __( 'Microsoft sign-in was cancelled.', 'm365-login' ),
+ 'provider_error' => __( 'Microsoft returned an error. Please try again.', 'm365-login' ),
+ 'token_exchange' => __( 'Could not complete the sign-in with Microsoft. Please try again or contact an administrator.', 'm365-login' ),
+ 'invalid_token' => __( 'The Microsoft sign-in could not be verified.', 'm365-login' ),
+ 'no_email' => __( 'Your Microsoft account did not provide an e-mail address.', 'm365-login' ),
+ 'domain_not_allowed' => __( 'Your e-mail domain is not allowed to sign in here.', 'm365-login' ),
+ 'no_user' => __( 'No WordPress account exists for your Microsoft e-mail address.', 'm365-login' ),
+ 'oid_mismatch' => __( 'This WordPress account is linked to a different Microsoft account. Please contact an administrator.', 'm365-login' ),
+ 'not_allowed' => __( 'You are not allowed to sign in with this account.', 'm365-login' ),
+ 'not_in_group' => __( 'Your Microsoft account is not a member of a group that is allowed to sign in here.', 'm365-login' ),
+ 'group_check_failed' => __( 'Your group membership could not be verified. Please contact an administrator.', 'm365-login' ),
+ 'in_denied_group' => __( 'Your Microsoft account is a member of a group that is not allowed to sign in here.', 'm365-login' ),
+ 'fallback_invalid' => __( 'The fallback key is not valid.', 'm365-login' ),
+ 'fallback_locked' => __( 'Too many attempts. Please wait 15 minutes.', 'm365-login' ),
+ 'too_many_attempts' => __( 'Too many sign-in attempts from your connection. Please wait a few minutes and try again.', 'm365-login' ),
+ 'account_disabled' => __( 'This account has been deactivated.', 'm365-login' ),
+ 'privileged_unlinked' => __( 'For security reasons this administrator account can only be linked to a Microsoft account whose user principal name equals the WordPress e-mail address. Please contact an administrator.', 'm365-login' ),
+ 'external_identity' => __( 'Guest and external accounts cannot sign in here.', 'm365-login' ),
);
}
diff --git a/includes/class-m365-login-certificate.php b/includes/class-m365-login-certificate.php
index fd0653b..1ece8d3 100644
--- a/includes/class-m365-login-certificate.php
+++ b/includes/class-m365-login-certificate.php
@@ -97,6 +97,9 @@ final class M365_Login_Certificate {
return new WP_Error( 'key_bits', __( 'The RSA key must have at least 2048 bits.', 'm365-login' ) );
}
+ if ( false !== strpos( $cert_pem, 'PRIVATE KEY' ) ) {
+ return new WP_Error( 'cert_has_key', __( 'The certificate field contains a private key. Paste only the certificate (-----BEGIN CERTIFICATE-----) there.', 'm365-login' ) );
+ }
$cert = openssl_x509_read( $cert_pem );
if ( false === $cert ) {
return new WP_Error( 'cert_invalid', self::openssl_error( __( 'The certificate could not be read. Paste it in PEM format (-----BEGIN CERTIFICATE-----).', 'm365-login' ) ) );
@@ -110,9 +113,15 @@ final class M365_Login_Certificate {
return new WP_Error( 'cert_expired', __( 'The certificate has already expired.', 'm365-login' ) );
}
+ // Store exactly one clean certificate (drops chains, bundles and surrounding text).
+ $clean = '';
+ if ( ! openssl_x509_export( $cert, $clean ) || '' === $clean ) {
+ return new WP_Error( 'cert_invalid', self::openssl_error( __( 'The certificate could not be read. Paste it in PEM format (-----BEGIN CERTIFICATE-----).', 'm365-login' ) ) );
+ }
+
return array(
'private_key' => $key_pem,
- 'certificate' => $cert_pem,
+ 'certificate' => self::normalise_pem( $clean ),
);
}
diff --git a/includes/class-m365-login-crypto.php b/includes/class-m365-login-crypto.php
index e29a2dc..8765657 100644
--- a/includes/class-m365-login-crypto.php
+++ b/includes/class-m365-login-crypto.php
@@ -11,7 +11,9 @@ defined( 'ABSPATH' ) || exit;
* AES-256-GCM helper keyed from the WordPress salts.
*
* The key is derived from AUTH_KEY / SECURE_AUTH_KEY (via wp_salt()), so the
- * stored client secret is useless without access to wp-config.php.
+ * stored client secret is useless without access to wp-config.php – provided the
+ * salts are defined there. Without them wp_salt() keeps generated salts in the
+ * database next to the ciphertext; the settings screen warns about that.
*/
final class M365_Login_Crypto {
diff --git a/includes/class-m365-login-graph.php b/includes/class-m365-login-graph.php
index f4f8bfb..19dba22 100644
--- a/includes/class-m365-login-graph.php
+++ b/includes/class-m365-login-graph.php
@@ -107,9 +107,10 @@ class M365_Login_Graph {
* @param string $path Path relative to the v1.0 base (with query string) or an absolute Graph URL (paging links).
* @param array|null $json JSON body for POST requests.
* @param array $headers Extra headers.
+ * @param bool $retry Retry on 429/503/504.
* @return array|WP_Error Response array from wp_remote_request().
*/
- private function raw_request( $method, $path, $json = null, $headers = array() ) {
+ private function raw_request( $method, $path, $json = null, $headers = array(), $retry = true ) {
$url = 0 === strpos( $path, self::GRAPH_BASE . '/' ) ? $path : self::GRAPH_BASE . $path;
if ( 0 !== strpos( $url, self::GRAPH_BASE . '/' ) ) {
return new WP_Error( 'graph_bad_url', 'Refusing to call a non-Graph URL.' );
@@ -146,7 +147,7 @@ class M365_Login_Graph {
if ( 401 === $code ) {
$this->flush_token();
}
- if ( $attempt < 4 && in_array( $code, array( 429, 503, 504 ), true ) ) {
+ if ( $retry && $attempt < 4 && in_array( $code, array( 429, 503, 504 ), true ) ) {
$wait = (int) wp_remote_retrieve_header( $response, 'retry-after' );
sleep( max( 1, min( 10, $wait > 0 ? $wait : $attempt * 2 ) ) );
continue;
@@ -162,10 +163,11 @@ class M365_Login_Graph {
* @param string $path Path relative to the v1.0 base (with query string).
* @param array|null $json JSON body for POST requests.
* @param array $headers Extra headers.
+ * @param bool $retry Retry on 429/503/504.
* @return array|WP_Error Decoded JSON. Errors carry array( 'status' => HTTP code ) as data.
*/
- private function request( $method, $path, $json = null, $headers = array() ) {
- $response = $this->raw_request( $method, $path, $json, $headers );
+ private function request( $method, $path, $json = null, $headers = array(), $retry = true ) {
+ $response = $this->raw_request( $method, $path, $json, $headers, $retry );
if ( is_wp_error( $response ) ) {
return $response;
}
@@ -390,10 +392,10 @@ class M365_Login_Graph {
*/
public function search_groups( $query ) {
$query = trim( (string) $query );
- $select = '$select=id,displayName,description,securityEnabled,mailEnabled&$top=25&$orderby=displayName';
+ $select = '$select=id,displayName,description,securityEnabled,mailEnabled,groupTypes,visibility&$top=25&$orderby=displayName';
if ( '' !== $query && M365_Login_Settings::is_guid( $query ) ) {
- $path = '/groups/' . rawurlencode( strtolower( $query ) ) . '?$select=id,displayName,description,securityEnabled,mailEnabled';
+ $path = '/groups/' . rawurlencode( strtolower( $query ) ) . '?$select=id,displayName,description,securityEnabled,mailEnabled,groupTypes,visibility';
$item = $this->request( 'GET', $path );
if ( is_wp_error( $item ) ) {
return $item;
@@ -437,6 +439,10 @@ class M365_Login_Graph {
} elseif ( ! empty( $item['mailEnabled'] ) ) {
$type = __( 'Microsoft 365 group', 'm365-login' );
}
+ $unified = isset( $item['groupTypes'] ) && is_array( $item['groupTypes'] ) && in_array( 'Unified', $item['groupTypes'], true );
+ if ( $unified && isset( $item['visibility'] ) && 'Public' === $item['visibility'] ) {
+ $type = __( 'Public Microsoft 365 group – anyone in the organisation can join', 'm365-login' );
+ }
return array(
'id' => strtolower( (string) $item['id'] ),
'name' => isset( $item['displayName'] ) ? (string) $item['displayName'] : (string) $item['id'],
@@ -450,9 +456,10 @@ class M365_Login_Graph {
*
* @param string $user_oid User object ID.
* @param string[] $group_ids Group object IDs (any count; chunked by 20).
+ * @param bool $retry Retry on throttling (off in the interactive sign-in).
* @return string[]|WP_Error Matching group IDs.
*/
- public function check_member_groups( $user_oid, $group_ids ) {
+ public function check_member_groups( $user_oid, $group_ids, $retry = true ) {
if ( ! M365_Login_Settings::is_guid( $user_oid ) ) {
return new WP_Error( 'graph_bad_oid', 'Invalid user object ID.' );
}
@@ -462,7 +469,9 @@ class M365_Login_Graph {
$result = $this->request(
'POST',
'/users/' . rawurlencode( strtolower( $user_oid ) ) . '/checkMemberGroups',
- array( 'groupIds' => $chunk )
+ array( 'groupIds' => $chunk ),
+ array(),
+ $retry
);
if ( is_wp_error( $result ) ) {
return $result;
diff --git a/includes/class-m365-login-settings.php b/includes/class-m365-login-settings.php
index 566450b..e07a5a1 100644
--- a/includes/class-m365-login-settings.php
+++ b/includes/class-m365-login-settings.php
@@ -402,7 +402,10 @@ class M365_Login_Settings {
if ( defined( 'M365_LOGIN_DISABLE_BUTTON_ONLY' ) && M365_LOGIN_DISABLE_BUTTON_ONLY ) {
return false;
}
- return $this->is_configured() && (bool) $this->get( 'button_only' ) && '' !== $this->fallback_key();
+ // Deliberately not tied to is_configured(): an expired certificate or rotated salts must not
+ // silently re-enable password sign-in. The fallback link and the constant stay available.
+ return '' !== (string) $this->get( 'tenant_id' ) && '' !== (string) $this->get( 'client_id' )
+ && (bool) $this->get( 'button_only' ) && '' !== $this->fallback_key();
}
/**
@@ -497,7 +500,7 @@ class M365_Login_Settings {
$out['client_id'] = strtolower( $client_id );
// Client secret: only replaced when a new value was entered.
- $secret_input = isset( $input['client_secret'] ) ? (string) wp_unslash( $input['client_secret'] ) : '';
+ $secret_input = self::scalar( $input, 'client_secret' );
$secret_input = trim( $secret_input );
if ( ! empty( $input['client_secret_clear'] ) ) {
$out['client_secret'] = '';
@@ -520,8 +523,8 @@ class M365_Login_Settings {
// Certificate: keep the stored pair unless a new one is pasted or removal is requested.
$out['cert_private_key'] = $current['cert_private_key'];
$out['cert_certificate'] = $current['cert_certificate'];
- $pasted_key = isset( $input['cert_key_pem'] ) ? trim( (string) wp_unslash( $input['cert_key_pem'] ) ) : '';
- $pasted_cert = isset( $input['cert_cert_pem'] ) ? trim( (string) wp_unslash( $input['cert_cert_pem'] ) ) : '';
+ $pasted_key = trim( self::scalar( $input, 'cert_key_pem' ) );
+ $pasted_cert = trim( self::scalar( $input, 'cert_cert_pem' ) );
if ( ! empty( $input['cert_remove'] ) ) {
$out['cert_private_key'] = '';
$out['cert_certificate'] = '';
@@ -573,7 +576,7 @@ class M365_Login_Settings {
$out['fallback_key'] = $key;
// Custom login page (must be on this site).
- $custom = isset( $input['custom_login_url'] ) ? esc_url_raw( trim( wp_unslash( $input['custom_login_url'] ) ) ) : '';
+ $custom = esc_url_raw( trim( self::scalar( $input, 'custom_login_url' ) ) );
if ( '' !== $custom ) {
if ( 0 === strpos( $custom, '/' ) ) {
$custom = home_url( $custom );
@@ -590,13 +593,13 @@ class M365_Login_Settings {
$text = isset( $input['button_text'] ) ? sanitize_text_field( wp_unslash( $input['button_text'] ) ) : '';
$out['button_text'] = '' === trim( $text ) ? $defaults['button_text'] : mb_substr( $text, 0, 80 );
- $icon = isset( $input['button_icon'] ) ? esc_url_raw( trim( wp_unslash( $input['button_icon'] ) ) ) : '';
+ $icon = esc_url_raw( trim( self::scalar( $input, 'button_icon' ) ) );
$out['button_icon'] = self::is_safe_image_url( $icon ) ? $icon : '';
$out['button_show_icon'] = empty( $input['button_show_icon'] ) ? 0 : 1;
foreach ( array( 'button_bg', 'button_bg_hover', 'button_color', 'button_border' ) as $color_key ) {
- $color = isset( $input[ $color_key ] ) ? sanitize_hex_color( trim( wp_unslash( $input[ $color_key ] ) ) ) : '';
+ $color = sanitize_hex_color( trim( self::scalar( $input, $color_key ) ) );
$out[ $color_key ] = $color ? $color : $defaults[ $color_key ];
}
@@ -692,6 +695,17 @@ class M365_Login_Settings {
return $out;
}
+ /**
+ * Unslashed string value of a posted field ('' for missing or non-scalar values such as arrays).
+ *
+ * @param array $input Raw input.
+ * @param string $key Field.
+ * @return string
+ */
+ private static function scalar( $input, $key ) {
+ return isset( $input[ $key ] ) && is_scalar( $input[ $key ] ) ? (string) wp_unslash( $input[ $key ] ) : '';
+ }
+
/**
* Sanitises a GUID => name list posted by a group picker.
*
diff --git a/includes/class-m365-login-sync.php b/includes/class-m365-login-sync.php
index 347b23a..87e67ab 100644
--- a/includes/class-m365-login-sync.php
+++ b/includes/class-m365-login-sync.php
@@ -15,8 +15,10 @@ defined( 'ABSPATH' ) || exit;
class M365_Login_Sync {
const CRON_HOOK = 'm365_login_sync';
- const LOCK = 'm365_login_sync_lock';
- const LOCK_TTL = 30 * MINUTE_IN_SECONDS;
+ const LOCK = 'm365_login_sync_lock'; // Option (atomic via add_option), value "token|time".
+ const LOCK_TTL = 2 * HOUR_IN_SECONDS;
+ const TENANT_OPTION = 'm365_login_sync_tenant'; // Tenant of the last successful live run.
+ const PHOTO_FILE = '#^m365-login-avatars/m365-[a-f0-9]{16}-[a-f0-9]{8}\.(?:jpg|png|gif)$#';
const REPORT_OPTION = 'm365_login_sync_report';
const META_SYNCED = '_m365_login_synced'; // Account was created by the sync.
const META_DISABLED = '_m365_login_disabled'; // Time, origin (sync or manual) and reason.
@@ -55,6 +57,13 @@ class M365_Login_Sync {
*/
private $dry = false;
+ /**
+ * Further accounts bound to the same object ID as the one in linked_users() (oid => user IDs).
+ *
+ * @var array
+ */
+ private $duplicates = array();
+
/**
* Constructor.
*
@@ -305,11 +314,11 @@ class M365_Login_Sync {
'log' => array(),
);
- if ( get_transient( self::LOCK ) ) {
+ $lock = $this->acquire_lock();
+ if ( '' === $lock ) {
$this->log( 'error', __( 'Another sync is still running. Please try again in a few minutes.', 'm365-login' ) );
return $this->finish( 'locked', false );
}
- set_transient( self::LOCK, time(), self::LOCK_TTL );
if ( function_exists( 'set_time_limit' ) ) {
set_time_limit( 0 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- long-running directory sync.
@@ -317,21 +326,51 @@ class M365_Login_Sync {
wp_raise_memory_limit( 'admin' );
require_once ABSPATH . 'wp-admin/includes/user.php';
- // No "your e-mail/password changed" mails for changes made by the sync.
- add_filter( 'send_email_change_email', '__return_false', 99 );
+ // No "your password changed" mails for the random passwords of deactivated accounts.
+ // E-mail change notifications stay on: the previous address is told about the change.
add_filter( 'send_password_change_email', '__return_false', 99 );
try {
$status = $this->sync();
} finally {
- remove_filter( 'send_email_change_email', '__return_false', 99 );
remove_filter( 'send_password_change_email', '__return_false', 99 );
- delete_transient( self::LOCK );
+ $this->release_lock( $lock );
}
return $this->finish( $status, true );
}
+ /**
+ * Takes the run lock atomically (add_option fails when the row exists).
+ *
+ * @return string Lock token, or '' when another run holds the lock.
+ */
+ private function acquire_lock() {
+ $token = wp_generate_password( 20, false );
+ if ( add_option( self::LOCK, $token . '|' . time(), '', 'no' ) ) {
+ return $token;
+ }
+ $held = explode( '|', (string) get_option( self::LOCK, '' ) );
+ if ( isset( $held[1] ) && time() - (int) $held[1] < self::LOCK_TTL ) {
+ return '';
+ }
+ // Stale lock of a crashed run.
+ delete_option( self::LOCK );
+ return add_option( self::LOCK, $token . '|' . time(), '', 'no' ) ? $token : '';
+ }
+
+ /**
+ * Releases the run lock if this run still holds it.
+ *
+ * @param string $token Lock token.
+ */
+ private function release_lock( $token ) {
+ $held = explode( '|', (string) get_option( self::LOCK, '' ) );
+ if ( $held[0] === $token ) {
+ delete_option( self::LOCK );
+ }
+ }
+
/**
* Stores and returns the report.
*
@@ -392,7 +431,12 @@ class M365_Login_Sync {
}
// 2. Create, link and update accounts.
- $linked = $this->linked_users();
+ $linked = $this->linked_users();
+ if ( empty( $people ) && ! empty( $linked ) ) {
+ $this->log( 'error', __( 'Microsoft 365 returned no users at all while accounts are linked. Nothing was changed. Check the tenant and the sync groups.', 'm365-login' ) );
+ return 'aborted';
+ }
+
$seen = array();
$pending = array(); // Deactivations/deletions, applied after the safety check.
$photo_of = array(); // oid => WP_User whose photo is kept in sync.
@@ -404,7 +448,7 @@ class M365_Login_Sync {
$result = $this->sync_person( $person, $linked, $memberships );
if ( is_array( $result ) ) {
- $pending[] = $result;
+ $pending = array_merge( $pending, $result );
} elseif ( $result instanceof WP_User ) {
$photo_of[ $oid ] = $result;
}
@@ -428,13 +472,22 @@ class M365_Login_Sync {
return 'failed';
}
if ( null !== $action ) {
- $pending[] = $action;
+ $pending = array_merge( $pending, $action );
}
}
// 4. Safety net: never deactivate or delete a large part of the linked accounts in one go.
$pending = array_values( array_filter( $pending, array( $this, 'is_effective_action' ) ) );
- $limit = (int) apply_filters( 'm365_login_sync_deprovision_limit', max( 5, (int) ceil( count( $linked ) * 0.2 ) ), count( $linked ) );
+ $tenant = strtolower( $this->settings->tenant() );
+ $before = (string) get_option( self::TENANT_OPTION, '' );
+ if ( $pending && '' !== $before && $before !== $tenant ) {
+ $this->log( 'warning', __( 'The tenant ID changed since the last sync. Accounts linked in the old tenant are not found in the new one, so no account was deactivated or deleted in this run. Run the sync again to apply deactivations.', 'm365-login' ) );
+ $pending = array();
+ }
+ if ( ! $this->dry ) {
+ update_option( self::TENANT_OPTION, $tenant, false );
+ }
+ $limit = (int) apply_filters( 'm365_login_sync_deprovision_limit', max( 5, (int) ceil( count( $linked ) * 0.2 ) ), count( $linked ) );
if ( count( $pending ) > $limit ) {
$this->log(
'error',
@@ -519,7 +572,7 @@ class M365_Login_Sync {
* @return array oid => user ID.
*/
private function linked_users() {
- $users = get_users(
+ $users = get_users(
array(
'meta_key' => M365_Login_Auth::META_OID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_compare' => 'EXISTS',
@@ -527,10 +580,16 @@ class M365_Login_Sync {
'number' => -1,
)
);
- $out = array();
+ $out = array();
+ $this->duplicates = array();
foreach ( $users as $row ) {
$oid = strtolower( (string) get_user_meta( (int) $row->ID, M365_Login_Auth::META_OID, true ) );
- if ( M365_Login_Settings::is_guid( $oid ) ) {
+ if ( ! M365_Login_Settings::is_guid( $oid ) ) {
+ continue;
+ }
+ if ( isset( $out[ $oid ] ) ) {
+ $this->duplicates[ $oid ][] = (int) $row->ID;
+ } else {
$out[ $oid ] = (int) $row->ID;
}
}
@@ -556,6 +615,12 @@ class M365_Login_Sync {
$user = isset( $linked[ $oid ] ) ? get_userdata( $linked[ $oid ] ) : false;
+ // A linked person disabled in Microsoft 365 is handled before anything else, so a
+ // removed or changed e-mail address during offboarding cannot keep the account alive.
+ if ( $user && ! $enabled ) {
+ return $this->action( (string) $this->settings->get( 'sync_disabled_action' ), $user, 'disabled', __( 'disabled in Microsoft 365', 'm365-login' ), $oid );
+ }
+
if ( ! $user && isset( $person['userType'] ) && 'Guest' === $person['userType'] && ! $this->settings->get( 'sync_guests' ) ) {
return null; // Guests are not imported (they may still be linked through a sign-in).
}
@@ -586,6 +651,11 @@ class M365_Login_Sync {
$this->skip( sprintf( __( '%s: the WordPress account with this e-mail address is linked to a different Microsoft account, skipped.', 'm365-login' ), $email ) );
return null;
}
+ if ( ! $this->may_link( $by_mail, $person, $email ) ) {
+ /* translators: %s: e-mail address */
+ $this->skip( sprintf( __( '%s: privileged WordPress account – it is only linked when the Microsoft user principal name equals its e-mail address (member account, no guest). Skipped.', 'm365-login' ), $email ) );
+ return null;
+ }
$user = $by_mail;
$this->log( 'info', sprintf( /* translators: %s: e-mail address */ __( '%s: existing account linked.', 'm365-login' ), $email ) );
$this->count( 'linked' );
@@ -601,7 +671,7 @@ class M365_Login_Sync {
if ( ! $user ) {
return null; // Nothing to create for disabled people.
}
- return $this->action( (string) $this->settings->get( 'sync_disabled_action' ), $user, 'disabled', __( 'disabled in Microsoft 365', 'm365-login' ) );
+ return $this->action( (string) $this->settings->get( 'sync_disabled_action' ), $user, 'disabled', __( 'disabled in Microsoft 365', 'm365-login' ), $oid );
}
if ( ! $user ) {
@@ -626,7 +696,7 @@ class M365_Login_Sync {
}
$changes = $this->update_profile( $user, $person, $email );
- if ( $this->manages_roles( $user ) ) {
+ if ( $this->manages_roles( $user ) && ! self::disabled_info( $user->ID ) ) {
$changes = array_merge( $changes, $this->update_roles( $user, $this->desired_roles( $oid, $memberships ) ) );
}
@@ -741,7 +811,10 @@ class M365_Login_Sync {
if ( strtolower( $user->user_email ) !== $email ) {
$owner = get_user_by( 'email', $email );
- if ( $owner && $owner->ID !== $user->ID ) {
+ if ( self::is_privileged( $user ) ) {
+ /* translators: 1: current e-mail address, 2: e-mail address in Microsoft 365 */
+ $this->log( 'warning', sprintf( __( '%1$s: the e-mail address in Microsoft 365 changed to %2$s. It is not changed automatically for privileged accounts – update it by hand if intended.', 'm365-login' ), $user->user_email, $email ) );
+ } elseif ( $owner && $owner->ID !== $user->ID ) {
/* translators: %s: e-mail address */
$this->log( 'warning', sprintf( __( '%s: e-mail address is used by another WordPress account and was not changed.', 'm365-login' ), $email ) );
} else {
@@ -935,7 +1008,7 @@ class M365_Login_Sync {
*
* @param string $oid Object ID.
* @param int $user_id User ID.
- * @return array|null|WP_Error Pending action, null for none.
+ * @return array[]|null|WP_Error Pending actions, null for none.
*/
private function classify_missing( $oid, $user_id ) {
$user = get_userdata( $user_id );
@@ -947,15 +1020,15 @@ class M365_Login_Sync {
$person = $this->graph->get_user( $oid, array( 'id', 'accountEnabled', 'userType' ) );
if ( is_wp_error( $person ) ) {
if ( M365_Login_Graph::is_not_found( $person ) ) {
- return $this->action( (string) $this->settings->get( 'sync_deleted_action' ), $user, 'deleted', __( 'deleted in Microsoft 365', 'm365-login' ) );
+ return $this->action( (string) $this->settings->get( 'sync_deleted_action' ), $user, 'deleted', __( 'deleted in Microsoft 365', 'm365-login' ), $oid );
}
return $person;
}
if ( isset( $person['accountEnabled'] ) && false === $person['accountEnabled'] ) {
- return $this->action( (string) $this->settings->get( 'sync_disabled_action' ), $user, 'disabled', __( 'disabled in Microsoft 365', 'm365-login' ) );
+ return $this->action( (string) $this->settings->get( 'sync_disabled_action' ), $user, 'disabled', __( 'disabled in Microsoft 365', 'm365-login' ), $oid );
}
if ( $this->settings->sync_scope_groups() && ( ! isset( $person['userType'] ) || 'Guest' !== $person['userType'] || $this->settings->get( 'sync_guests' ) ) ) {
- return $this->action( (string) $this->settings->get( 'sync_scope_action' ), $user, 'scope', __( 'no longer a member of the sync groups', 'm365-login' ) );
+ return $this->action( (string) $this->settings->get( 'sync_scope_action' ), $user, 'scope', __( 'no longer a member of the sync groups', 'm365-login' ), $oid );
}
return null;
}
@@ -967,23 +1040,79 @@ class M365_Login_Sync {
* @param WP_User $user User.
* @param string $reason Machine reason.
* @param string $label Human reason.
- * @return array|null
+ * @param string $oid Object ID (further accounts bound to it get the same action).
+ * @return array[]|null Pending actions.
*/
- private function action( $what, $user, $reason, $label ) {
+ private function action( $what, $user, $reason, $label, $oid = '' ) {
if ( ! in_array( $what, array( 'disable', 'delete' ), true ) ) {
return null;
}
- if ( $this->is_protected( $user ) ) {
- /* translators: 1: e-mail address, 2: reason */
- $this->skip( sprintf( __( '%1$s: %2$s, but the account is protected (administrator or your own account) and was not changed.', 'm365-login' ), $user->user_email, $label ) );
- return null;
+ $users = array( $user );
+ if ( '' !== $oid && ! empty( $this->duplicates[ $oid ] ) ) {
+ foreach ( $this->duplicates[ $oid ] as $user_id ) {
+ $other = get_userdata( $user_id );
+ if ( $other && $other->ID !== $user->ID ) {
+ $users[] = $other;
+ }
+ }
}
- return array(
- 'what' => $what,
- 'user' => $user,
- 'reason' => $reason,
- 'label' => $label,
- );
+ $out = array();
+ foreach ( $users as $target ) {
+ if ( $this->is_protected( $target ) ) {
+ /* translators: 1: e-mail address, 2: reason */
+ $this->skip( sprintf( __( '%1$s: %2$s, but the account is protected (administrator or your own account) and was not changed.', 'm365-login' ), $target->user_email, $label ) );
+ continue;
+ }
+ $out[] = array(
+ 'what' => $what,
+ 'user' => $target,
+ 'reason' => $reason,
+ 'label' => $label,
+ );
+ }
+ return $out ? $out : null;
+ }
+
+ /**
+ * Whether an existing account may be linked to a directory user by e-mail address.
+ *
+ * Privileged accounts are only linked through the user principal name of a member
+ * account: its domain must be verified in the tenant, whereas the "mail" attribute can
+ * be set to any address by a user or Exchange administrator of the tenant.
+ *
+ * @param WP_User $user Existing account.
+ * @param array $person Graph user.
+ * @param string $email Address the account was found by.
+ * @return bool
+ */
+ private function may_link( $user, $person, $email ) {
+ if ( ! self::is_privileged( $user ) ) {
+ return true;
+ }
+ $upn = isset( $person['userPrincipalName'] ) ? strtolower( (string) $person['userPrincipalName'] ) : '';
+ $guest = isset( $person['userType'] ) && 'Guest' === $person['userType'];
+ return ! $guest && '' !== $upn && false === strpos( $upn, '#ext#' ) && strtolower( $user->user_email ) === $upn && $upn === $email;
+ }
+
+ /**
+ * Accounts with administrative capabilities (they get extra protection against linking by e-mail).
+ *
+ * @param WP_User $user User.
+ * @return bool
+ */
+ public static function is_privileged( $user ) {
+ $privileged = is_super_admin( $user->ID )
+ || user_can( $user, 'manage_options' )
+ || user_can( $user, 'promote_users' )
+ || user_can( $user, 'edit_users' );
+
+ /**
+ * Filters whether an account counts as privileged (linked only via a matching user principal name).
+ *
+ * @param bool $privileged Whether the account is privileged.
+ * @param WP_User $user User.
+ */
+ return (bool) apply_filters( 'm365_login_is_privileged_user', $privileged, $user );
}
/**
@@ -1215,7 +1344,7 @@ class M365_Login_Sync {
continue;
}
- if ( ! empty( $stored['file'] ) && isset( $stored['etag'] ) && $stored['etag'] === $version && file_exists( self::photo_path( $stored['file'] ) ) ) {
+ if ( ! empty( $stored['file'] ) && isset( $stored['etag'] ) && $stored['etag'] === $version && self::is_photo_file( $stored['file'] ) && file_exists( self::photo_path( $stored['file'] ) ) ) {
if ( ! $this->dry ) {
$stored['checked'] = time();
update_user_meta( $user->ID, self::META_PHOTO, $stored );
@@ -1256,7 +1385,7 @@ class M365_Login_Sync {
continue;
}
- if ( ! empty( $stored['file'] ) && $stored['file'] !== $file ) {
+ if ( ! empty( $stored['file'] ) && $stored['file'] !== $file && self::is_photo_file( $stored['file'] ) ) {
wp_delete_file( self::photo_path( $stored['file'] ) );
}
update_user_meta(
@@ -1388,6 +1517,16 @@ class M365_Login_Sync {
return trailingslashit( $uploads['basedir'] ) . ltrim( $file, '/' );
}
+ /**
+ * Whether a stored photo path is one the plugin wrote (no traversal, fixed folder and pattern).
+ *
+ * @param mixed $file Relative path from user meta.
+ * @return bool
+ */
+ private static function is_photo_file( $file ) {
+ return is_string( $file ) && (bool) preg_match( self::PHOTO_FILE, $file );
+ }
+
/**
* Deletes a user's stored photo (also hooked to user deletion).
*
@@ -1395,7 +1534,7 @@ class M365_Login_Sync {
*/
public function delete_photo( $user_id ) {
$stored = get_user_meta( $user_id, self::META_PHOTO, true );
- if ( is_array( $stored ) && ! empty( $stored['file'] ) && 0 === strpos( $stored['file'], self::PHOTO_DIR . '/' ) ) {
+ if ( is_array( $stored ) && ! empty( $stored['file'] ) && self::is_photo_file( $stored['file'] ) ) {
wp_delete_file( self::photo_path( $stored['file'] ) );
}
delete_user_meta( $user_id, self::META_PHOTO );
@@ -1431,7 +1570,7 @@ class M365_Login_Sync {
}
$stored = get_user_meta( $user_id, self::META_PHOTO, true );
- if ( ! is_array( $stored ) || empty( $stored['file'] ) ) {
+ if ( ! is_array( $stored ) || empty( $stored['file'] ) || ! self::is_photo_file( $stored['file'] ) ) {
return $args;
}
$uploads = wp_get_upload_dir();
@@ -1472,6 +1611,9 @@ class M365_Login_Sync {
* @param string $reason Machine reason.
*/
public static function disable( $user_id, $by, $reason = '' ) {
+ $user = get_userdata( $user_id );
+ $roles = $user ? array_values( $user->roles ) : array();
+
update_user_meta(
$user_id,
self::META_DISABLED,
@@ -1479,9 +1621,20 @@ class M365_Login_Sync {
'time' => time(),
'by' => $by,
'reason' => $reason,
+ 'roles' => $roles,
)
);
+
+ // Lock the account for good, also without this plugin: no sessions, no role on this
+ // site, a random password nobody knows and no application passwords.
WP_Session_Tokens::get_instance( $user_id )->destroy_all();
+ if ( class_exists( 'WP_Application_Passwords' ) ) {
+ WP_Application_Passwords::delete_all_application_passwords( $user_id );
+ }
+ wp_set_password( wp_generate_password( 64, true, true ), $user_id );
+ if ( $user ) {
+ $user->set_role( '' );
+ }
/**
* Fires after an account was deactivated.
@@ -1499,8 +1652,19 @@ class M365_Login_Sync {
* @param int $user_id User ID.
*/
public static function enable( $user_id ) {
+ $info = self::disabled_info( $user_id );
delete_user_meta( $user_id, self::META_DISABLED );
+ // Give back the roles taken away on deactivation (the sync may adjust them afterwards).
+ $user = get_userdata( $user_id );
+ if ( $user && empty( $user->roles ) && $info && ! empty( $info['roles'] ) && is_array( $info['roles'] ) ) {
+ foreach ( $info['roles'] as $role ) {
+ if ( is_string( $role ) && get_role( $role ) ) {
+ $user->add_role( $role );
+ }
+ }
+ }
+
/**
* Fires after an account was reactivated.
*
diff --git a/languages/m365-login-de_DE.mo b/languages/m365-login-de_DE.mo
index efb30b8..28dbcc1 100644
Binary files a/languages/m365-login-de_DE.mo and b/languages/m365-login-de_DE.mo differ
diff --git a/languages/m365-login-de_DE.po b/languages/m365-login-de_DE.po
index a358e8c..181e001 100644
--- a/languages/m365-login-de_DE.po
+++ b/languages/m365-login-de_DE.po
@@ -16,1133 +16,1161 @@ msgstr ""
"X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n"
-#: includes/class-m365-login-admin.php:92 includes/class-m365-login-admin.php:93 includes/class-m365-login-admin.php:104 includes/class-m365-login-admin.php:725
+#: includes/class-m365-login-admin.php:108 includes/class-m365-login-admin.php:109 includes/class-m365-login-admin.php:120 includes/class-m365-login-admin.php:778
msgid "M365 Login"
msgstr "M365 Login"
-#: includes/class-m365-login-admin.php:119
+#: includes/class-m365-login-admin.php:135
msgid "Connection"
msgstr "Verbindung"
-#: includes/class-m365-login-admin.php:120
+#: includes/class-m365-login-admin.php:136
msgid "Button"
msgstr "Button"
-#: includes/class-m365-login-admin.php:121
+#: includes/class-m365-login-admin.php:137
msgid "Security"
msgstr "Sicherheit"
-#: includes/class-m365-login-admin.php:122
+#: includes/class-m365-login-admin.php:138
msgid "User sync"
msgstr "Benutzer-Sync"
-#: includes/class-m365-login-admin.php:197
-msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
-msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
+#: includes/class-m365-login-admin.php:207
+msgid "M365 Login: button-only mode is on, but the connection to Microsoft is broken (missing or undecryptable secret, or expired certificate). Nobody can sign in except through the fallback link."
+msgstr "M365 Login: Der Nur-Button-Modus ist aktiv, aber die Verbindung zu Microsoft ist gestört (Secret fehlt oder ist nicht entschlüsselbar, oder das Zertifikat ist abgelaufen). Anmelden ist nur noch über den Fallback-Link möglich."
-#: includes/class-m365-login-admin.php:199
+#: includes/class-m365-login-admin.php:209 includes/class-m365-login-admin.php:227
msgid "Open the settings"
msgstr "Einstellungen öffnen"
-#: includes/class-m365-login-admin.php:230
+#: includes/class-m365-login-admin.php:225
+msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
+msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
+
+#: includes/class-m365-login-admin.php:263
msgid "Choose button icon"
msgstr "Button-Icon auswählen"
-#: includes/class-m365-login-admin.php:231
+#: includes/class-m365-login-admin.php:264
msgid "Use this icon"
msgstr "Dieses Icon verwenden"
-#: includes/class-m365-login-admin.php:232
+#: includes/class-m365-login-admin.php:265
msgid "Copied!"
msgstr "Kopiert!"
-#: includes/class-m365-login-admin.php:233 includes/class-m365-login-admin.php:848 includes/class-m365-login-admin.php:1112 includes/class-m365-login-admin.php:1157
+#: includes/class-m365-login-admin.php:266 includes/class-m365-login-admin.php:904 includes/class-m365-login-admin.php:1171 includes/class-m365-login-admin.php:1216
msgid "Copy"
msgstr "Kopieren"
-#: includes/class-m365-login-admin.php:234
+#: includes/class-m365-login-admin.php:267
msgid "Testing…"
msgstr "Wird geprüft …"
-#: includes/class-m365-login-admin.php:235
+#: includes/class-m365-login-admin.php:268
msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections."
msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen."
-#: includes/class-m365-login-admin.php:236
+#: includes/class-m365-login-admin.php:269
msgid "No groups found."
msgstr "Keine Gruppen gefunden."
-#: includes/class-m365-login-admin.php:237
+#: includes/class-m365-login-admin.php:270
msgid "Searching…"
msgstr "Suche läuft …"
-#: includes/class-m365-login-admin.php:238
+#: includes/class-m365-login-admin.php:271
msgid "Add"
msgstr "Hinzufügen"
-#: includes/class-m365-login-admin.php:239 includes/class-m365-login-admin.php:495
+#: includes/class-m365-login-admin.php:272 includes/class-m365-login-admin.php:528
msgid "Remove"
msgstr "Entfernen"
-#: includes/class-m365-login-admin.php:240 includes/class-m365-login-admin.php:303 includes/class-m365-login-admin.php:468
+#: includes/class-m365-login-admin.php:273 includes/class-m365-login-admin.php:336 includes/class-m365-login-admin.php:501
msgid "Save the connection settings first, then search for groups."
msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen."
-#: includes/class-m365-login-admin.php:241
+#: includes/class-m365-login-admin.php:274
msgid "Generate a new fallback key on save? The old link stops working."
msgstr "Beim Speichern einen neuen Fallback-Schlüssel erzeugen? Der alte Link funktioniert dann nicht mehr."
-#: includes/class-m365-login-admin.php:242
+#: includes/class-m365-login-admin.php:275
msgid "Generating a 3072-bit key pair, this takes a moment…"
msgstr "3072-Bit-Schlüsselpaar wird erzeugt, das dauert einen Moment …"
-#: includes/class-m365-login-admin.php:243
+#: includes/class-m365-login-admin.php:276
msgid "Replace the stored certificate? Sign-in stops working until the new certificate is uploaded to Entra ID."
msgstr "Gespeichertes Zertifikat ersetzen? Die Anmeldung funktioniert erst wieder, wenn das neue Zertifikat in Entra ID hochgeladen ist."
-#: includes/class-m365-login-admin.php:244
+#: includes/class-m365-login-admin.php:277
msgid "Remove the stored certificate when saving? Sign-in with the certificate method stops working."
msgstr "Gespeichertes Zertifikat beim Speichern entfernen? Die Anmeldung per Zertifikat funktioniert dann nicht mehr."
-#: includes/class-m365-login-admin.php:245
+#: includes/class-m365-login-admin.php:278
msgid "Sync is running, this can take a while for large directories…"
msgstr "Sync läuft, bei großen Verzeichnissen kann das etwas dauern …"
-#: includes/class-m365-login-admin.php:246
+#: includes/class-m365-login-admin.php:279
msgid "Run the sync now with the saved settings? Accounts are created, updated and possibly deactivated or deleted. Tip: run a dry run first."
msgstr "Sync jetzt mit den gespeicherten Einstellungen ausführen? Konten werden angelegt, aktualisiert und eventuell deaktiviert oder gelöscht. Tipp: Führe zuerst einen Testlauf aus."
-#: includes/class-m365-login-admin.php:247
+#: includes/class-m365-login-admin.php:280
msgid "The request failed or timed out. Reload the page in a few minutes to see the report; for very large directories use \"wp m365-login sync\" (WP-CLI)."
msgstr "Die Anfrage ist fehlgeschlagen oder hat zu lange gedauert. Lade die Seite in ein paar Minuten neu, um den Bericht zu sehen; für sehr große Verzeichnisse nutze „wp m365-login sync“ (WP-CLI)."
-#: includes/class-m365-login-admin.php:248
+#: includes/class-m365-login-admin.php:281
msgid "You have unsaved changes. The sync uses the saved settings – save first."
msgstr "Du hast ungespeicherte Änderungen. Der Sync verwendet die gespeicherten Einstellungen – speichere zuerst."
-#: includes/class-m365-login-admin.php:249 includes/class-m365-login-admin.php:482
+#: includes/class-m365-login-admin.php:282 includes/class-m365-login-admin.php:515
msgid "Move up"
msgstr "Nach oben"
-#: includes/class-m365-login-admin.php:261 includes/class-m365-login-admin.php:300 includes/class-m365-login-admin.php:326 includes/class-m365-login-admin.php:359 includes/class-m365-login-admin.php:514 includes/class-m365-login-sync.php:1611
+#: includes/class-m365-login-admin.php:294 includes/class-m365-login-admin.php:333 includes/class-m365-login-admin.php:359 includes/class-m365-login-admin.php:392 includes/class-m365-login-admin.php:566 includes/class-m365-login-sync.php:1775
msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:266
+#: includes/class-m365-login-admin.php:299
msgid "Please enter a valid tenant ID first."
msgstr "Bitte zuerst eine gültige Tenant-ID eingeben."
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:280
+#: includes/class-m365-login-admin.php:313
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?"
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:289
+#: includes/class-m365-login-admin.php:322
msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen."
-#: includes/class-m365-login-admin.php:312
+#: includes/class-m365-login-admin.php:345
msgid "Microsoft Graph refused the request. Grant the application permission \"GroupMember.Read.All\" (or \"Directory.Read.All\") with admin consent in Entra ID."
msgstr "Microsoft Graph hat die Anfrage abgelehnt. In Entra ID die Anwendungsberechtigung „GroupMember.Read.All“ (oder „Directory.Read.All“) mit Administratorzustimmung erteilen."
-#: includes/class-m365-login-admin.php:330
+#: includes/class-m365-login-admin.php:363
msgid "Unknown operation."
msgstr "Unbekannte Aktion."
-#: includes/class-m365-login-admin.php:347
+#: includes/class-m365-login-admin.php:380
msgid "Certificate generated and stored. Download the .cer file and upload it in Entra ID."
msgstr "Zertifikat erzeugt und gespeichert. Jetzt die .cer-Datei herunterladen und in Entra ID hochladen."
-#: includes/class-m365-login-admin.php:374
+#: includes/class-m365-login-admin.php:407
msgid "The sync has not run yet."
msgstr "Der Sync ist noch nicht gelaufen."
-#: includes/class-m365-login-admin.php:378
+#: includes/class-m365-login-admin.php:411
msgid "Finished"
msgstr "Abgeschlossen"
-#: includes/class-m365-login-admin.php:379
+#: includes/class-m365-login-admin.php:412
msgid "Failed"
msgstr "Fehlgeschlagen"
-#: includes/class-m365-login-admin.php:380
+#: includes/class-m365-login-admin.php:413
msgid "Stopped by the safety limit"
msgstr "Vom Sicherheitslimit gestoppt"
-#: includes/class-m365-login-admin.php:381
+#: includes/class-m365-login-admin.php:414
msgid "Not started"
msgstr "Nicht gestartet"
-#: includes/class-m365-login-admin.php:384
+#: includes/class-m365-login-admin.php:417
msgid "started manually"
msgstr "manuell gestartet"
-#: includes/class-m365-login-admin.php:385
+#: includes/class-m365-login-admin.php:418
msgid "scheduled"
msgstr "geplant"
-#: includes/class-m365-login-admin.php:386
+#: includes/class-m365-login-admin.php:419
msgid "WP-CLI"
msgstr "WP-CLI"
-#: includes/class-m365-login-admin.php:389
+#: includes/class-m365-login-admin.php:422
msgid "would be created"
msgstr "würden angelegt"
-#: includes/class-m365-login-admin.php:389
+#: includes/class-m365-login-admin.php:422
msgid "created"
msgstr "angelegt"
-#: includes/class-m365-login-admin.php:390
+#: includes/class-m365-login-admin.php:423
msgid "would be updated"
msgstr "würden aktualisiert"
-#: includes/class-m365-login-admin.php:390
+#: includes/class-m365-login-admin.php:423
msgid "updated"
msgstr "aktualisiert"
-#: includes/class-m365-login-admin.php:391
+#: includes/class-m365-login-admin.php:424
msgid "would be linked"
msgstr "würden verknüpft"
-#: includes/class-m365-login-admin.php:391
+#: includes/class-m365-login-admin.php:424
msgid "linked"
msgstr "verknüpft"
-#: includes/class-m365-login-admin.php:392
+#: includes/class-m365-login-admin.php:425
msgid "unchanged"
msgstr "unverändert"
-#: includes/class-m365-login-admin.php:393
+#: includes/class-m365-login-admin.php:426
msgid "would be deactivated"
msgstr "würden deaktiviert"
-#: includes/class-m365-login-admin.php:393
+#: includes/class-m365-login-admin.php:426
msgid "deactivated"
msgstr "deaktiviert"
-#: includes/class-m365-login-admin.php:394
+#: includes/class-m365-login-admin.php:427
msgid "would be reactivated"
msgstr "würden reaktiviert"
-#: includes/class-m365-login-admin.php:394
+#: includes/class-m365-login-admin.php:427
msgid "reactivated"
msgstr "reaktiviert"
-#: includes/class-m365-login-admin.php:395
+#: includes/class-m365-login-admin.php:428
msgid "would be deleted"
msgstr "würden gelöscht"
-#: includes/class-m365-login-admin.php:395
+#: includes/class-m365-login-admin.php:428
msgid "deleted"
msgstr "gelöscht"
-#: includes/class-m365-login-admin.php:396
+#: includes/class-m365-login-admin.php:429
msgid "photos"
msgstr "Profilbilder"
-#: includes/class-m365-login-admin.php:397
+#: includes/class-m365-login-admin.php:430
msgid "skipped"
msgstr "übersprungen"
-#: includes/class-m365-login-admin.php:398
+#: includes/class-m365-login-admin.php:431
msgid "errors"
msgstr "Fehler"
-#: includes/class-m365-login-admin.php:411
+#: includes/class-m365-login-admin.php:444
msgid "Dry run – nothing was changed"
msgstr "Testlauf – nichts wurde geändert"
#. translators: 1: date and time, 2: how the run was started, 3: duration in seconds
-#: includes/class-m365-login-admin.php:416
+#: includes/class-m365-login-admin.php:449
msgid "%1$s, %2$s, %3$d s"
msgstr "%1$s, %2$s, %3$d s"
#. translators: %d: number of log entries
-#: includes/class-m365-login-admin.php:434
+#: includes/class-m365-login-admin.php:467
msgid "Log (%d entry)"
msgid_plural "Log (%d entries)"
msgstr[0] "Protokoll (%d Eintrag)"
msgstr[1] "Protokoll (%d Einträge)"
-#: includes/class-m365-login-admin.php:462
+#: includes/class-m365-login-admin.php:495
msgid "Search groups"
msgstr "Gruppen suchen"
-#: includes/class-m365-login-admin.php:464
+#: includes/class-m365-login-admin.php:497
msgid "Type a group name or paste an object ID…"
msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
-#: includes/class-m365-login-admin.php:465
+#: includes/class-m365-login-admin.php:498
msgid "Search"
msgstr "Suchen"
-#: includes/class-m365-login-admin.php:470
+#: includes/class-m365-login-admin.php:503
msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
-#: includes/class-m365-login-admin.php:476
+#: includes/class-m365-login-admin.php:509
msgid "Selected groups"
msgstr "Ausgewählte Gruppen"
-#: includes/class-m365-login-admin.php:488
+#: includes/class-m365-login-admin.php:521
msgid "WordPress role"
msgstr "WordPress-Rolle"
-#: includes/class-m365-login-admin.php:520
+#: includes/class-m365-login-admin.php:572
msgid "No certificate is stored."
msgstr "Es ist kein Zertifikat gespeichert."
-#: includes/class-m365-login-admin.php:545
+#: includes/class-m365-login-admin.php:597
msgid "Do nothing"
msgstr "Nichts tun"
-#: includes/class-m365-login-admin.php:546
+#: includes/class-m365-login-admin.php:598
msgid "Deactivate the WordPress account"
msgstr "WordPress-Konto deaktivieren"
-#: includes/class-m365-login-admin.php:547
+#: includes/class-m365-login-admin.php:599
msgid "Delete the WordPress account"
msgstr "WordPress-Konto löschen"
-#: includes/class-m365-login-admin.php:550
+#: includes/class-m365-login-admin.php:602
msgid "Account disabled in Microsoft 365 (sign-in blocked)"
msgstr "Konto in Microsoft 365 deaktiviert (Anmeldung blockiert)"
-#: includes/class-m365-login-admin.php:551
+#: includes/class-m365-login-admin.php:603
msgid "Account deleted in Microsoft 365"
msgstr "Konto in Microsoft 365 gelöscht"
-#: includes/class-m365-login-admin.php:552
+#: includes/class-m365-login-admin.php:604
msgid "No longer a member of the sync groups"
msgstr "Kein Mitglied der Sync-Gruppen mehr"
-#: includes/class-m365-login-admin.php:557
+#: includes/class-m365-login-admin.php:609
msgid "Import users from Microsoft 365"
msgstr "Benutzer aus Microsoft 365 importieren"
-#: includes/class-m365-login-admin.php:558
+#: includes/class-m365-login-admin.php:610
msgid "Creates a WordPress account for every Microsoft 365 user in scope, links existing accounts by e-mail address, keeps roles and profile fields up to date and deactivates or deletes accounts that were disabled or removed in Microsoft 365. New accounts get a random password and no e-mail; people sign in with the Microsoft button."
msgstr "Legt für jeden Microsoft-365-Benutzer im Geltungsbereich ein WordPress-Konto an, verknüpft bestehende Konten über die E-Mail-Adresse, hält Rollen und Profilfelder aktuell und deaktiviert oder löscht Konten, die in Microsoft 365 deaktiviert oder entfernt wurden. Neue Konten erhalten ein Zufallspasswort und keine E-Mail; die Anmeldung erfolgt über den Microsoft-Button."
-#: includes/class-m365-login-admin.php:563
+#: includes/class-m365-login-admin.php:615
msgid "Run the sync automatically"
msgstr "Sync automatisch ausführen"
-#: includes/class-m365-login-admin.php:564
+#: includes/class-m365-login-admin.php:616
msgid "Uses WP-Cron, which runs when the site receives visits. For exact timing, trigger wp-cron.php from a real cron job or run \"wp m365-login sync\"."
msgstr "Nutzt WP-Cron, das bei Besuchen der Website ausgelöst wird. Für genaue Zeiten rufe wp-cron.php über einen echten Cronjob auf oder führe „wp m365-login sync“ aus."
-#: includes/class-m365-login-admin.php:570
+#: includes/class-m365-login-admin.php:622
msgid "Interval"
msgstr "Intervall"
-#: includes/class-m365-login-admin.php:572
+#: includes/class-m365-login-admin.php:624
msgid "Hourly"
msgstr "Stündlich"
-#: includes/class-m365-login-admin.php:573
+#: includes/class-m365-login-admin.php:625
msgid "Twice daily"
msgstr "Zweimal täglich"
-#: includes/class-m365-login-admin.php:574
+#: includes/class-m365-login-admin.php:626
msgid "Daily"
msgstr "Täglich"
#. translators: %s: date and time
-#: includes/class-m365-login-admin.php:578
+#: includes/class-m365-login-admin.php:630
msgid "Next run: %s"
msgstr "Nächster Lauf: %s"
-#: includes/class-m365-login-admin.php:586
+#: includes/class-m365-login-admin.php:638
msgid "Also import guest users (B2B)"
msgstr "Auch Gastbenutzer importieren (B2B)"
-#: includes/class-m365-login-admin.php:587
+#: includes/class-m365-login-admin.php:639
msgid "Guests are external people invited into your tenant. Off by default."
msgstr "Gäste sind externe Personen, die in deinen Tenant eingeladen wurden. Standardmäßig aus."
-#: includes/class-m365-login-admin.php:591
+#: includes/class-m365-login-admin.php:643
msgid "Which users? (optional)"
msgstr "Welche Benutzer? (optional)"
-#: includes/class-m365-login-admin.php:592
+#: includes/class-m365-login-admin.php:644
msgid "Limit the import to members of these groups (nested memberships count). Without groups, every user of the tenant is imported. The e-mail domain allow-list on the Security tab applies as well."
msgstr "Beschränkt den Import auf Mitglieder dieser Gruppen (verschachtelte Mitgliedschaften zählen). Ohne Gruppen wird jeder Benutzer des Tenants importiert. Die Liste erlaubter E-Mail-Domains im Tab „Sicherheit“ gilt ebenfalls."
-#: includes/class-m365-login-admin.php:593
+#: includes/class-m365-login-admin.php:645
msgid "No groups selected – all users of the tenant are imported."
msgstr "Keine Gruppen ausgewählt – alle Benutzer des Tenants werden importiert."
-#: includes/class-m365-login-admin.php:597
+#: includes/class-m365-login-admin.php:649
msgid "Roles"
msgstr "Rollen"
-#: includes/class-m365-login-admin.php:600
+#: includes/class-m365-login-admin.php:652
msgid "Default role"
msgstr "Standardrolle"
-#: includes/class-m365-login-admin.php:604
+#: includes/class-m365-login-admin.php:656
msgid "Every imported user gets this role. The sync manages the roles of imported accounts – manual role changes are overwritten on the next run."
msgstr "Jeder importierte Benutzer erhält diese Rolle. Die Rollen importierter Konten verwaltet der Sync – manuelle Rollenänderungen werden beim nächsten Lauf überschrieben."
-#: includes/class-m365-login-admin.php:607
+#: includes/class-m365-login-admin.php:659
msgid "Additional roles from Microsoft 365 groups"
msgstr "Zusätzliche Rollen aus Microsoft-365-Gruppen"
-#: includes/class-m365-login-admin.php:608
+#: includes/class-m365-login-admin.php:660
msgid "Members of a group (nested memberships count) get the role next to it. If a person leaves the group, the role is removed again on the next sync."
msgstr "Mitglieder einer Gruppe (verschachtelte Mitgliedschaften zählen) erhalten die Rolle daneben. Verlässt eine Person die Gruppe, wird die Rolle beim nächsten Sync wieder entfernt."
-#: includes/class-m365-login-admin.php:609
+#: includes/class-m365-login-admin.php:661
+msgid "Whoever can change a group's members controls the mapped role. For roles with administrative rights use security groups (ideally role-assignable ones) – never public Microsoft 365 groups or Teams, which members can join themselves."
+msgstr "Wer die Mitglieder einer Gruppe ändern kann, bestimmt über die zugeordnete Rolle. Für Rollen mit Administrationsrechten Sicherheitsgruppen verwenden (am besten rollenzuweisbare) – nie öffentliche Microsoft-365-Gruppen oder Teams, denen Mitglieder selbst beitreten können."
+
+#: includes/class-m365-login-admin.php:662
msgid "No group mapping – everybody gets the default role."
msgstr "Keine Gruppenzuordnung – alle erhalten die Standardrolle."
-#: includes/class-m365-login-admin.php:612
+#: includes/class-m365-login-admin.php:665
msgid "How are mapped roles applied?"
msgstr "Wie werden zugeordnete Rollen vergeben?"
-#: includes/class-m365-login-admin.php:615
+#: includes/class-m365-login-admin.php:668
msgid "In addition to the default role (a user can have several roles)"
msgstr "Zusätzlich zur Standardrolle (ein Benutzer kann mehrere Rollen haben)"
-#: includes/class-m365-login-admin.php:619
+#: includes/class-m365-login-admin.php:672
msgid "Instead of the default role – the first matching group in the list wins (use ↑ to reorder)"
msgstr "Anstelle der Standardrolle – die erste passende Gruppe der Liste gewinnt (Reihenfolge mit ↑ ändern)"
-#: includes/class-m365-login-admin.php:626
+#: includes/class-m365-login-admin.php:679
msgid "Also manage the roles of accounts that existed before the sync"
msgstr "Auch die Rollen von Konten verwalten, die schon vor dem Sync existierten"
-#: includes/class-m365-login-admin.php:627
+#: includes/class-m365-login-admin.php:680
msgid "Off: existing accounts are only linked and get their profile fields updated; their roles stay as they are. Administrators that existed before the sync and your own account are never changed."
msgstr "Aus: Bestehende Konten werden nur verknüpft und ihre Profilfelder aktualisiert; ihre Rollen bleiben, wie sie sind. Administratoren, die schon vor dem Sync existierten, und dein eigenes Konto werden nie verändert."
-#: includes/class-m365-login-admin.php:633
+#: includes/class-m365-login-admin.php:686
msgid "Profile fields"
msgstr "Profilfelder"
-#: includes/class-m365-login-admin.php:634
+#: includes/class-m365-login-admin.php:687
msgid "Selected Microsoft 365 attributes are copied into the WordPress profile on every sync (Microsoft 365 wins). Name fields go into the standard profile fields, everything else into user meta keys starting with \"m365_\" – usable by themes and other plugins – and is shown on the profile screen."
msgstr "Ausgewählte Microsoft-365-Attribute werden bei jedem Sync ins WordPress-Profil übernommen (Microsoft 365 hat Vorrang). Namen landen in den normalen Profilfeldern, alles andere in Benutzer-Metadaten mit dem Präfix „m365_“ – nutzbar für Themes und andere Plugins – und wird auf der Profilseite angezeigt."
-#: includes/class-m365-login-admin.php:643
+#: includes/class-m365-login-admin.php:696
msgid "Profile photos are stored in wp-content/uploads/m365-login-avatars/ and replace the Gravatar. They are compared on every run: changed photos are downloaded again, photos deleted in Microsoft 365 are deleted in WordPress too. Fields and photos you deselect here are removed from the profiles on the next run (first and last name and display name stay)."
msgstr "Profilbilder werden in wp-content/uploads/m365-login-avatars/ gespeichert und ersetzen den Gravatar. Sie werden bei jedem Lauf abgeglichen: Geänderte Bilder werden neu geladen, in Microsoft 365 gelöschte Bilder auch in WordPress gelöscht. Felder und Bilder, die du hier abwählst, werden beim nächsten Lauf aus den Profilen entfernt (Vor-, Nach- und Anzeigename bleiben)."
-#: includes/class-m365-login-admin.php:647
+#: includes/class-m365-login-admin.php:700
msgid "Disabled and deleted Microsoft 365 accounts"
msgstr "Deaktivierte und gelöschte Microsoft-365-Konten"
-#: includes/class-m365-login-admin.php:648
+#: includes/class-m365-login-admin.php:701
msgid "Applies to WordPress accounts linked to a Microsoft account (imported, or signed in with Microsoft at least once). Deactivated accounts cannot sign in at all – not with Microsoft, a password or an application password – and are signed out immediately. When the person is active in Microsoft 365 again, the sync reactivates the account."
msgstr "Gilt für WordPress-Konten, die mit einem Microsoft-Konto verknüpft sind (importiert oder mindestens einmal per Microsoft angemeldet). Deaktivierte Konten können sich gar nicht mehr anmelden – weder mit Microsoft noch mit Passwort oder Anwendungspasswort – und werden sofort abgemeldet. Ist die Person in Microsoft 365 wieder aktiv, reaktiviert der Sync das Konto."
-#: includes/class-m365-login-admin.php:659
+#: includes/class-m365-login-admin.php:712
msgid "Only relevant when the import is limited to groups."
msgstr "Nur relevant, wenn der Import auf Gruppen beschränkt ist."
-#: includes/class-m365-login-admin.php:665
+#: includes/class-m365-login-admin.php:718
msgid "Posts of deleted accounts go to"
msgstr "Beiträge gelöschter Konten übernimmt"
-#: includes/class-m365-login-admin.php:673
+#: includes/class-m365-login-admin.php:726
msgid "— Select a user —"
msgstr "— Benutzer auswählen —"
-#: includes/class-m365-login-admin.php:680
+#: includes/class-m365-login-admin.php:733
msgid "Required for \"Delete\". Without a user, accounts are deactivated instead, so no content is ever lost."
msgstr "Erforderlich für „Löschen“. Ohne Benutzer werden Konten stattdessen deaktiviert, damit nie Inhalte verloren gehen."
-#: includes/class-m365-login-admin.php:683
+#: includes/class-m365-login-admin.php:736
msgid "Safety stop: if a run would deactivate or delete more than 20 % of the linked accounts (at least 5), nothing is deactivated or deleted and the run is reported as stopped. A failed Microsoft Graph request also stops the run before anything is deactivated."
msgstr "Sicherheitsstopp: Würde ein Lauf mehr als 20 % der verknüpften Konten (mindestens 5) deaktivieren oder löschen, wird nichts deaktiviert oder gelöscht und der Lauf als gestoppt gemeldet. Auch eine fehlgeschlagene Microsoft-Graph-Anfrage stoppt den Lauf, bevor etwas deaktiviert wird."
-#: includes/class-m365-login-admin.php:687
+#: includes/class-m365-login-admin.php:740
msgid "Run the sync"
msgstr "Sync ausführen"
-#: includes/class-m365-login-admin.php:688
+#: includes/class-m365-login-admin.php:741
msgid "The run uses the saved settings. Start with a dry run: it reads Microsoft 365 and lists what would change, without changing anything."
msgstr "Der Lauf verwendet die gespeicherten Einstellungen. Beginne mit einem Testlauf: Er liest Microsoft 365 und listet auf, was sich ändern würde, ohne etwas zu ändern."
-#: includes/class-m365-login-admin.php:690
+#: includes/class-m365-login-admin.php:743
msgid "Dry run"
msgstr "Testlauf"
-#: includes/class-m365-login-admin.php:691
+#: includes/class-m365-login-admin.php:744
msgid "Sync now"
msgstr "Jetzt synchronisieren"
-#: includes/class-m365-login-admin.php:693
+#: includes/class-m365-login-admin.php:746
msgid "Required application permissions (Microsoft Graph, admin consent): User.Read.All, and GroupMember.Read.All when groups are used."
msgstr "Benötigte Anwendungsberechtigungen (Microsoft Graph, Administratorzustimmung): User.Read.All, bei Verwendung von Gruppen zusätzlich GroupMember.Read.All."
-#: includes/class-m365-login-admin.php:707
+#: includes/class-m365-login-admin.php:760
msgid "You are not allowed to access this page."
msgstr "Für diese Seite fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:726
+#: includes/class-m365-login-admin.php:779
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr "Bestehende Benutzer melden sich mit ihrem Microsoft 365 / Entra ID-Konto an."
-#: includes/class-m365-login-admin.php:731
+#: includes/class-m365-login-admin.php:784
msgid "Connected"
msgstr "Verbunden"
-#: includes/class-m365-login-admin.php:731
+#: includes/class-m365-login-admin.php:784
msgid "Setup incomplete"
msgstr "Einrichtung unvollständig"
-#: includes/class-m365-login-admin.php:753
+#: includes/class-m365-login-admin.php:806
msgid "Microsoft Entra ID app registration"
msgstr "App-Registrierung in Microsoft Entra ID"
-#: includes/class-m365-login-admin.php:754
+#: includes/class-m365-login-admin.php:807
msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr "Trage hier die Werte aus deiner App-Registrierung im Microsoft Entra Admin Center ein."
-#: includes/class-m365-login-admin.php:757
+#: includes/class-m365-login-admin.php:810
msgid "Directory (tenant) ID"
msgstr "Verzeichnis-ID (Mandant/Tenant)"
-#: includes/class-m365-login-admin.php:760
+#: includes/class-m365-login-admin.php:813
msgid "Test tenant"
msgstr "Tenant testen"
-#: includes/class-m365-login-admin.php:762
+#: includes/class-m365-login-admin.php:815
msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
msgstr "Empfohlen: die GUID deines Tenants. Dann werden nur Anmeldungen aus diesem Tenant akzeptiert. „organizations“ erlaubt beliebige Geschäfts-, Schul- oder Unikonten."
-#: includes/class-m365-login-admin.php:764
+#: includes/class-m365-login-admin.php:817
msgid "Multi-tenant mode: accounts from any Microsoft tenant can sign in. Their \"email\" attribute is not verified, so the plugin matches on the user principal name (verified domain) only and ignores the e-mail claim unless Microsoft marks it as domain-verified. Use the e-mail domain allow-list on the Security tab, or better, pin your tenant GUID."
msgstr "Multi-Tenant-Modus: Konten aus beliebigen Microsoft-Tenants können sich anmelden. Deren „email“-Attribut ist nicht verifiziert, deshalb ordnet das Plugin nur über den User Principal Name (verifizierte Domain) zu und ignoriert den E-Mail-Claim, sofern Microsoft ihn nicht als domain-verifiziert markiert. Nutze die Domain-Allowlist im Tab „Sicherheit“ oder besser: die Tenant-GUID eintragen."
-#: includes/class-m365-login-admin.php:770
+#: includes/class-m365-login-admin.php:823
msgid "Application (client) ID"
msgstr "Anwendungs-ID (Client)"
-#: includes/class-m365-login-admin.php:775
+#: includes/class-m365-login-admin.php:828
msgid "How should WordPress authenticate to Microsoft?"
msgstr "Wie soll sich WordPress bei Microsoft authentifizieren?"
-#: includes/class-m365-login-admin.php:780 includes/class-m365-login-admin.php:797
+#: includes/class-m365-login-admin.php:833 includes/class-m365-login-admin.php:850
msgid "Client secret"
msgstr "Geheimer Clientschlüssel (Client Secret)"
-#: includes/class-m365-login-admin.php:781
+#: includes/class-m365-login-admin.php:834
msgid "Quick to set up. A password-like value created in Entra ID that expires after 6–24 months and must be renewed."
msgstr "Schnell eingerichtet. Ein passwortähnlicher Wert aus Entra ID, der nach 6–24 Monaten abläuft und erneuert werden muss."
-#: includes/class-m365-login-admin.php:787
+#: includes/class-m365-login-admin.php:840
msgid "Certificate"
msgstr "Zertifikat"
-#: includes/class-m365-login-admin.php:787
+#: includes/class-m365-login-admin.php:840
msgid "Recommended"
msgstr "Empfohlen"
-#: includes/class-m365-login-admin.php:788
+#: includes/class-m365-login-admin.php:841
msgid "The private key never leaves this server; only the public certificate is uploaded to Entra ID. Generated here with one click, valid for 2 years."
msgstr "Der private Schlüssel verlässt diesen Server nie; nur das öffentliche Zertifikat wird in Entra ID hochgeladen. Mit einem Klick hier erzeugt, 2 Jahre gültig."
-#: includes/class-m365-login-admin.php:799
+#: includes/class-m365-login-admin.php:852
msgid "•••••••••••• (stored, leave empty to keep)"
msgstr "•••••••••••• (gespeichert – leer lassen, um zu behalten)"
-#: includes/class-m365-login-admin.php:799
+#: includes/class-m365-login-admin.php:852
msgid "Paste the secret value"
msgstr "Wert des Secrets einfügen"
-#: includes/class-m365-login-admin.php:800
+#: includes/class-m365-login-admin.php:853
msgid "Show secret"
msgstr "Secret anzeigen"
-#: includes/class-m365-login-admin.php:805
+#: includes/class-m365-login-admin.php:858
msgid "Remove the stored secret"
msgstr "Gespeichertes Secret entfernen"
-#: includes/class-m365-login-admin.php:808
+#: includes/class-m365-login-admin.php:861
msgid "Stored encrypted (AES-256-GCM, key derived from your WordPress salts) and never displayed again. Client secrets expire – note the expiry date in Entra ID."
msgstr "Wird verschlüsselt gespeichert (AES-256-GCM, Schlüssel aus den WordPress-Salts abgeleitet) und nie wieder angezeigt. Client Secrets laufen ab – Ablaufdatum in Entra ID notieren."
-#: includes/class-m365-login-admin.php:812
+#: includes/class-m365-login-admin.php:863
+msgid "AUTH_KEY and SECURE_AUTH_KEY are not defined in wp-config.php, so WordPress keeps its salts in the database – right next to the encrypted secret. Add the salts to wp-config.php to make the encryption effective."
+msgstr "AUTH_KEY und SECURE_AUTH_KEY sind nicht in der wp-config.php definiert, daher speichert WordPress seine Salts in der Datenbank – direkt neben dem verschlüsselten Secret. Trage die Salts in die wp-config.php ein, damit die Verschlüsselung wirkt."
+
+#: includes/class-m365-login-admin.php:868
msgid "Step-by-step: create a client secret in Entra ID"
msgstr "Schritt für Schritt: Client Secret in Entra ID erstellen"
-#: includes/class-m365-login-admin.php:815
+#: includes/class-m365-login-admin.php:871
msgid "Open entra.microsoft.com and sign in with an account that has the \"Application Administrator\" or \"Global Administrator\" role."
msgstr "entra.microsoft.com öffnen und mit einem Konto anmelden, das die Rolle „Anwendungsadministrator“ oder „Globaler Administrator“ hat."
-#: includes/class-m365-login-admin.php:816
+#: includes/class-m365-login-admin.php:872
msgid "Go to Identity → Applications → App registrations and open your app (or create it first, see the general guide in the sidebar)."
msgstr "Zu Identität → Anwendungen → App-Registrierungen wechseln und die App öffnen (oder zuerst anlegen, siehe allgemeine Anleitung in der Seitenleiste)."
-#: includes/class-m365-login-admin.php:817
+#: includes/class-m365-login-admin.php:873
msgid "In the left menu choose Certificates & secrets, then the tab Client secrets, and click New client secret."
msgstr "Im linken Menü „Zertifikate & Geheimnisse“ wählen, dann den Reiter „Geheime Clientschlüssel“ und auf „Neuer geheimer Clientschlüssel“ klicken."
-#: includes/class-m365-login-admin.php:818
+#: includes/class-m365-login-admin.php:874
msgid "Enter a description such as \"WordPress login\" and pick an expiry. Microsoft allows at most 24 months; put a reminder in your calendar two weeks before."
msgstr "Eine Beschreibung wie „WordPress Login“ eingeben und eine Gültigkeit wählen. Microsoft erlaubt maximal 24 Monate; zwei Wochen vor Ablauf eine Kalender-Erinnerung setzen."
-#: includes/class-m365-login-admin.php:819
+#: includes/class-m365-login-admin.php:875
msgid "Click Add. Copy the Value column immediately – it is shown only once. The Secret ID column is NOT what you need."
msgstr "Auf „Hinzufügen“ klicken. Die Spalte „Wert“ sofort kopieren – sie wird nur einmal angezeigt. Die Spalte „Geheimnis-ID“ ist NICHT der gesuchte Wert."
-#: includes/class-m365-login-admin.php:820
+#: includes/class-m365-login-admin.php:876
msgid "Paste the value into the Client secret field above and save this page."
msgstr "Den Wert oben in das Feld „Geheimer Clientschlüssel“ einfügen und diese Seite speichern."
-#: includes/class-m365-login-admin.php:822
+#: includes/class-m365-login-admin.php:878
msgid "When the secret expires, sign-ins fail with \"Could not complete the sign-in with Microsoft\". Create a new secret, paste it here, save, then delete the old one in Entra ID."
msgstr "Läuft das Secret ab, scheitern Anmeldungen mit „Die Anmeldung über Microsoft konnte nicht abgeschlossen werden“. Dann ein neues Secret erstellen, hier einfügen, speichern und das alte in Entra ID löschen."
-#: includes/class-m365-login-admin.php:835
+#: includes/class-m365-login-admin.php:891
msgid "Expired"
msgstr "Abgelaufen"
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:839
+#: includes/class-m365-login-admin.php:895
msgid "Expires in %d days"
msgstr "Läuft in %d Tagen ab"
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:842
+#: includes/class-m365-login-admin.php:898
msgid "Valid"
msgstr "Gültig"
-#: includes/class-m365-login-admin.php:847
+#: includes/class-m365-login-admin.php:903
msgid "Thumbprint (SHA-1)"
msgstr "Fingerabdruck (SHA-1)"
-#: includes/class-m365-login-admin.php:849
+#: includes/class-m365-login-admin.php:905
msgid "Subject"
msgstr "Antragsteller"
-#: includes/class-m365-login-admin.php:851
+#: includes/class-m365-login-admin.php:907
msgid "Key size"
msgstr "Schlüssellänge"
-#: includes/class-m365-login-admin.php:853
+#: includes/class-m365-login-admin.php:909
msgid "Valid until"
msgstr "Gültig bis"
-#: includes/class-m365-login-admin.php:857
+#: includes/class-m365-login-admin.php:913
msgid "Download certificate (.cer)"
msgstr "Zertifikat herunterladen (.cer)"
-#: includes/class-m365-login-admin.php:858
+#: includes/class-m365-login-admin.php:914
msgid "Generate new certificate"
msgstr "Neues Zertifikat erzeugen"
-#: includes/class-m365-login-admin.php:861
+#: includes/class-m365-login-admin.php:917
msgid "Remove certificate when saving"
msgstr "Zertifikat beim Speichern entfernen"
-#: includes/class-m365-login-admin.php:865
+#: includes/class-m365-login-admin.php:921
msgid "No certificate stored yet."
msgstr "Noch kein Zertifikat gespeichert."
-#: includes/class-m365-login-admin.php:867
+#: includes/class-m365-login-admin.php:923
msgid "Generate certificate"
msgstr "Zertifikat erzeugen"
-#: includes/class-m365-login-admin.php:868
+#: includes/class-m365-login-admin.php:924
msgid "3072-bit RSA, self-signed, valid for 2 years. The private key is stored encrypted and never shown or downloadable."
msgstr "3072 Bit RSA, selbstsigniert, 2 Jahre gültig. Der private Schlüssel wird verschlüsselt gespeichert und nie angezeigt oder zum Download angeboten."
-#: includes/class-m365-login-admin.php:872
+#: includes/class-m365-login-admin.php:928
msgid "Use your own certificate instead (paste PEM)"
msgstr "Stattdessen eigenes Zertifikat verwenden (PEM einfügen)"
-#: includes/class-m365-login-admin.php:875
+#: includes/class-m365-login-admin.php:931
msgid "Private key (PEM, unencrypted)"
msgstr "Privater Schlüssel (PEM, unverschlüsselt)"
-#: includes/class-m365-login-admin.php:879
+#: includes/class-m365-login-admin.php:935
msgid "Certificate (PEM)"
msgstr "Zertifikat (PEM)"
-#: includes/class-m365-login-admin.php:881
+#: includes/class-m365-login-admin.php:937
msgid "RSA, at least 2048 bits. The pair is validated and the key is encrypted when you save. Both fields stay empty afterwards."
msgstr "RSA, mindestens 2048 Bit. Beim Speichern wird das Paar geprüft und der Schlüssel verschlüsselt. Beide Felder bleiben danach leer."
-#: includes/class-m365-login-admin.php:887
+#: includes/class-m365-login-admin.php:943
msgid "Step-by-step: register the certificate in Entra ID"
msgstr "Schritt für Schritt: Zertifikat in Entra ID hinterlegen"
-#: includes/class-m365-login-admin.php:890
+#: includes/class-m365-login-admin.php:946
msgid "Click Generate certificate above (or paste your own). Then click Download certificate (.cer) – the file contains only the public part."
msgstr "Oben auf „Zertifikat erzeugen“ klicken (oder ein eigenes einfügen). Danach „Zertifikat herunterladen (.cer)“ – die Datei enthält nur den öffentlichen Teil."
-#: includes/class-m365-login-admin.php:891
+#: includes/class-m365-login-admin.php:947
msgid "Open entra.microsoft.com → Identity → Applications → App registrations and open your app."
msgstr "entra.microsoft.com → Identität → Anwendungen → App-Registrierungen öffnen und die App auswählen."
-#: includes/class-m365-login-admin.php:892
+#: includes/class-m365-login-admin.php:948
msgid "Choose Certificates & secrets in the left menu, then the tab Certificates, and click Upload certificate."
msgstr "Im linken Menü „Zertifikate & Geheimnisse“ wählen, dann den Reiter „Zertifikate“ und auf „Zertifikat hochladen“ klicken."
-#: includes/class-m365-login-admin.php:893
+#: includes/class-m365-login-admin.php:949
msgid "Select the downloaded .cer file, add a description such as \"WordPress login\" and click Add."
msgstr "Die heruntergeladene .cer-Datei auswählen, eine Beschreibung wie „WordPress Login“ eingeben und auf „Hinzufügen“ klicken."
-#: includes/class-m365-login-admin.php:894
+#: includes/class-m365-login-admin.php:950
msgid "Compare the thumbprint Entra ID shows with the thumbprint above – they must match exactly."
msgstr "Den in Entra ID angezeigten Fingerabdruck mit dem Fingerabdruck oben vergleichen – beide müssen exakt übereinstimmen."
-#: includes/class-m365-login-admin.php:895
+#: includes/class-m365-login-admin.php:951
msgid "Make sure Certificate is selected above and save this page. If a client secret was stored before, you may delete it in Entra ID now."
msgstr "Sicherstellen, dass oben „Zertifikat“ ausgewählt ist, und diese Seite speichern. War vorher ein Client Secret gespeichert, kann es jetzt in Entra ID gelöscht werden."
-#: includes/class-m365-login-admin.php:897
+#: includes/class-m365-login-admin.php:953
msgid "How it works: for every token request WordPress signs a short-lived JWT (client assertion) with the private key; Microsoft verifies it with the uploaded certificate. Nothing secret is ever transmitted."
msgstr "So funktioniert es: Für jede Token-Anfrage signiert WordPress ein kurzlebiges JWT (Client Assertion) mit dem privaten Schlüssel; Microsoft prüft es mit dem hochgeladenen Zertifikat. Es wird nie ein Geheimnis übertragen."
-#: includes/class-m365-login-admin.php:898
+#: includes/class-m365-login-admin.php:954
msgid "Before the certificate expires: generate a new one here, upload it to Entra ID (both may be registered at the same time), save, then remove the old one from Entra ID. Sign-ins keep working during the switch."
msgstr "Vor Ablauf des Zertifikats: hier ein neues erzeugen, in Entra ID hochladen (beide dürfen gleichzeitig hinterlegt sein), speichern und danach das alte in Entra ID entfernen. Anmeldungen funktionieren während des Wechsels weiter."
-#: includes/class-m365-login-admin.php:904
+#: includes/class-m365-login-admin.php:960
msgid "Account prompt"
msgstr "Kontoauswahl"
-#: includes/class-m365-login-admin.php:906
+#: includes/class-m365-login-admin.php:962
msgid "Always let the user pick an account (recommended)"
msgstr "Benutzer wählt immer ein Konto aus (empfohlen)"
-#: includes/class-m365-login-admin.php:907
+#: includes/class-m365-login-admin.php:963
msgid "Use the current Microsoft session if available"
msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden"
-#: includes/class-m365-login-admin.php:908
+#: includes/class-m365-login-admin.php:964
msgid "Always require re-entering credentials"
msgstr "Immer erneute Eingabe der Anmeldedaten verlangen"
-#: includes/class-m365-login-admin.php:917
+#: includes/class-m365-login-admin.php:973
msgid "Appearance"
msgstr "Darstellung"
-#: includes/class-m365-login-admin.php:920
+#: includes/class-m365-login-admin.php:976
msgid "Live preview"
msgstr "Live-Vorschau"
-#: includes/class-m365-login-admin.php:934
+#: includes/class-m365-login-admin.php:990
msgid "Button text"
msgstr "Button-Text"
-#: includes/class-m365-login-admin.php:938
+#: includes/class-m365-login-admin.php:994
msgid "Divider text"
msgstr "Trennlinien-Text"
-#: includes/class-m365-login-admin.php:940
+#: includes/class-m365-login-admin.php:996
msgid "Leave empty to hide the divider line."
msgstr "Leer lassen, um die Trennlinie auszublenden."
-#: includes/class-m365-login-admin.php:945
+#: includes/class-m365-login-admin.php:1001
msgid "Icon"
msgstr "Icon"
-#: includes/class-m365-login-admin.php:948
+#: includes/class-m365-login-admin.php:1004
msgid "Show an icon on the button"
msgstr "Icon auf dem Button anzeigen"
-#: includes/class-m365-login-admin.php:959
+#: includes/class-m365-login-admin.php:1015
msgid "Default: Microsoft logo"
msgstr "Standard: Microsoft-Logo"
-#: includes/class-m365-login-admin.php:961
+#: includes/class-m365-login-admin.php:1017
msgid "Choose from media library"
msgstr "Aus Mediathek wählen"
-#: includes/class-m365-login-admin.php:962
+#: includes/class-m365-login-admin.php:1018
msgid "Use Microsoft logo"
msgstr "Microsoft-Logo verwenden"
-#: includes/class-m365-login-admin.php:964
+#: includes/class-m365-login-admin.php:1020
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
msgstr "PNG, SVG, JPG oder WebP. Quadratische Bilder (z. B. 64×64 px) eignen sich am besten."
-#: includes/class-m365-login-admin.php:972
+#: includes/class-m365-login-admin.php:1028
msgid "Background"
msgstr "Hintergrund"
-#: includes/class-m365-login-admin.php:973
+#: includes/class-m365-login-admin.php:1029
msgid "Background (hover)"
msgstr "Hintergrund (Hover)"
-#: includes/class-m365-login-admin.php:974
+#: includes/class-m365-login-admin.php:1030
msgid "Text colour"
msgstr "Textfarbe"
-#: includes/class-m365-login-admin.php:975
+#: includes/class-m365-login-admin.php:1031
msgid "Border"
msgstr "Rahmen"
-#: includes/class-m365-login-admin.php:988
+#: includes/class-m365-login-admin.php:1044
msgid "Corner radius"
msgstr "Eckenradius"
-#: includes/class-m365-login-admin.php:992
+#: includes/class-m365-login-admin.php:1048
msgid "Position on the login page"
msgstr "Position auf der Login-Seite"
-#: includes/class-m365-login-admin.php:994
+#: includes/class-m365-login-admin.php:1050
msgid "Below the login form"
msgstr "Unter dem Login-Formular"
-#: includes/class-m365-login-admin.php:995
+#: includes/class-m365-login-admin.php:1051
msgid "Above the login form"
msgstr "Über dem Login-Formular"
-#: includes/class-m365-login-admin.php:1001
+#: includes/class-m365-login-admin.php:1057
msgid "Quick presets"
msgstr "Schnellauswahl"
-#: includes/class-m365-login-admin.php:1002
+#: includes/class-m365-login-admin.php:1058
msgid "Microsoft dark"
msgstr "Microsoft dunkel"
-#: includes/class-m365-login-admin.php:1003
+#: includes/class-m365-login-admin.php:1059
msgid "Microsoft light"
msgstr "Microsoft hell"
-#: includes/class-m365-login-admin.php:1004
+#: includes/class-m365-login-admin.php:1060
msgid "Azure blue"
msgstr "Azure-Blau"
-#: includes/class-m365-login-admin.php:1005
+#: includes/class-m365-login-admin.php:1061
msgid "WordPress blue"
msgstr "WordPress-Blau"
-#: includes/class-m365-login-admin.php:1009
+#: includes/class-m365-login-admin.php:1065
msgid "Custom login page"
msgstr "Eigene Login-Seite"
-#: includes/class-m365-login-admin.php:1010
+#: includes/class-m365-login-admin.php:1066
msgid "Using your own login page instead of wp-login.php? Tell the plugin where it is so error messages, the fallback link and the post-logout redirect point there."
msgstr "Eigene Login-Seite statt wp-login.php? Hier eintragen, damit Fehlermeldungen, der Fallback-Link und die Weiterleitung nach dem Abmelden dorthin zeigen."
-#: includes/class-m365-login-admin.php:1013
+#: includes/class-m365-login-admin.php:1069
msgid "URL of your login page"
msgstr "URL der Login-Seite"
-#: includes/class-m365-login-admin.php:1015
+#: includes/class-m365-login-admin.php:1071
msgid "Must be on this site. Leave empty to use wp-login.php."
msgstr "Muss auf dieser Website liegen. Leer lassen, um wp-login.php zu verwenden."
-#: includes/class-m365-login-admin.php:1021
+#: includes/class-m365-login-admin.php:1077
msgid "Add the button to every wp_login_form() form automatically"
msgstr "Button automatisch in jedes wp_login_form()-Formular einfügen"
-#: includes/class-m365-login-admin.php:1022
+#: includes/class-m365-login-admin.php:1078
msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below."
msgstr "Deckt Themes und Plugins ab, die die WordPress-Login-Formularfunktion verwenden. Page-Builder-Widgets benötigen den Shortcode oder die Template-Funktion unten."
-#: includes/class-m365-login-admin.php:1027
+#: includes/class-m365-login-admin.php:1083
msgid "Manual placement"
msgstr "Manuelle Platzierung"
-#: includes/class-m365-login-admin.php:1028
+#: includes/class-m365-login-admin.php:1084
msgid "Shortcode (block editor, page builders):"
msgstr "Shortcode (Block-Editor, Page Builder):"
-#: includes/class-m365-login-admin.php:1030
+#: includes/class-m365-login-admin.php:1086
msgid "Template function (theme files):"
msgstr "Template-Funktion (Theme-Dateien):"
-#: includes/class-m365-login-admin.php:1032
+#: includes/class-m365-login-admin.php:1088
msgid "Both show the error messages of the last attempt; use m365_login_messages() to place them separately."
msgstr "Beide zeigen die Fehlermeldungen des letzten Versuchs; mit m365_login_messages() lassen sie sich separat platzieren."
-#: includes/class-m365-login-admin.php:1040
+#: includes/class-m365-login-admin.php:1096
msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung"
-#: includes/class-m365-login-admin.php:1041
+#: includes/class-m365-login-admin.php:1097
msgid "Sign-in never creates users. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists – created by hand or imported by the user sync."
msgstr "Die Anmeldung legt nie Benutzer an. Eine Microsoft-Anmeldung klappt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert – von Hand angelegt oder vom Benutzer-Sync importiert."
-#: includes/class-m365-login-admin.php:1046
+#: includes/class-m365-login-admin.php:1102
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
-#: includes/class-m365-login-admin.php:1047
+#: includes/class-m365-login-admin.php:1103
msgid "On first sign-in the immutable Microsoft object ID is stored with the user. Later sign-ins with the same e-mail but a different Microsoft identity are rejected. Strongly recommended."
msgstr "Bei der ersten Anmeldung wird die unveränderliche Microsoft-Objekt-ID am Benutzer gespeichert. Spätere Anmeldungen mit gleicher E-Mail, aber anderer Microsoft-Identität werden abgelehnt. Dringend empfohlen."
-#: includes/class-m365-login-admin.php:1054
+#: includes/class-m365-login-admin.php:1110
msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen"
-#: includes/class-m365-login-admin.php:1055
+#: includes/class-m365-login-admin.php:1111
msgid "If the token contains no \"email\" claim, use the UPN (e.g. jane@contoso.com) when it is a valid e-mail address. Usually required for work accounts."
msgstr "Enthält das Token keinen „email“-Claim, wird der UPN (z. B. jane@contoso.com) verwendet, sofern er eine gültige E-Mail-Adresse ist. Für Geschäftskonten meist erforderlich."
-#: includes/class-m365-login-admin.php:1062
+#: includes/class-m365-login-admin.php:1118
msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
-#: includes/class-m365-login-admin.php:1063
+#: includes/class-m365-login-admin.php:1119
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
-#: includes/class-m365-login-admin.php:1068
+#: includes/class-m365-login-admin.php:1124
msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)"
-#: includes/class-m365-login-admin.php:1070
+#: includes/class-m365-login-admin.php:1126
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr "Eine oder mehrere Domains, getrennt durch Kommas oder Zeilenumbrüche. Leer lassen, um alle Domains des Tenants zuzulassen."
-#: includes/class-m365-login-admin.php:1075
+#: includes/class-m365-login-admin.php:1131
msgid "Allowed Entra groups (optional)"
msgstr "Erlaubte Entra-Gruppen (optional)"
-#: includes/class-m365-login-admin.php:1076
+#: includes/class-m365-login-admin.php:1132
msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
msgstr "Nur Mitglieder mindestens einer dieser Gruppen dürfen sich anmelden. Leer lassen, um alle zugeordneten Benutzer zuzulassen. Verschachtelte Mitgliedschaften zählen."
-#: includes/class-m365-login-admin.php:1078
+#: includes/class-m365-login-admin.php:1134
msgid "No groups selected – every matched user may sign in."
msgstr "Keine Gruppen ausgewählt – jeder zugeordnete Benutzer darf sich anmelden."
-#: includes/class-m365-login-admin.php:1080
+#: includes/class-m365-login-admin.php:1136
msgid "Membership is read from the \"groups\" claim of the ID token when present; otherwise the plugin asks Microsoft Graph (application permission \"User.Read.All\" or \"Directory.Read.All\"). If neither works, the sign-in is refused."
msgstr "Die Mitgliedschaft wird aus dem „groups“-Claim des ID-Tokens gelesen, falls vorhanden; andernfalls fragt das Plugin Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“). Funktioniert beides nicht, wird die Anmeldung abgelehnt."
-#: includes/class-m365-login-admin.php:1085
+#: includes/class-m365-login-admin.php:1141
msgid "Excluded Entra groups (optional)"
msgstr "Ausgeschlossene Entra-Gruppen (optional)"
-#: includes/class-m365-login-admin.php:1086
+#: includes/class-m365-login-admin.php:1142
msgid "Members of these groups can never sign in with Microsoft – even if they are in an allowed group. Nested memberships count."
msgstr "Mitglieder dieser Gruppen können sich nie per Microsoft anmelden – auch nicht, wenn sie in einer erlaubten Gruppe sind. Verschachtelte Mitgliedschaften zählen."
-#: includes/class-m365-login-admin.php:1088
+#: includes/class-m365-login-admin.php:1145
+msgid "Group rules need a pinned tenant ID (GUID) on the Connection tab. In multi-tenant mode the group check cannot ask Microsoft Graph, so every sign-in is refused while groups are selected here or above."
+msgstr "Gruppenregeln brauchen eine feste Tenant-ID (GUID) im Tab „Verbindung“. Im Multi-Tenant-Modus kann die Gruppenprüfung Microsoft Graph nicht fragen, deshalb wird jede Anmeldung abgelehnt, solange hier oder oben Gruppen ausgewählt sind."
+
+#: includes/class-m365-login-admin.php:1147
msgid "No groups excluded."
msgstr "Keine Gruppen ausgeschlossen."
-#: includes/class-m365-login-admin.php:1090
+#: includes/class-m365-login-admin.php:1149
msgid "The plugin asks Microsoft Graph on every sign-in (application permission \"User.Read.All\" or \"Directory.Read.All\"), because a \"groups\" claim may be filtered and cannot prove that someone is not a member. If the check fails, the sign-in is refused. Password sign-in is not affected – combine with button-only mode if needed."
msgstr "Das Plugin fragt bei jeder Anmeldung Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“), weil ein „groups“-Claim gefiltert sein kann und nicht beweist, dass jemand kein Mitglied ist. Schlägt die Prüfung fehl, wird die Anmeldung abgelehnt. Die Passwort-Anmeldung ist nicht betroffen – bei Bedarf mit dem Nur-Button-Modus kombinieren."
-#: includes/class-m365-login-admin.php:1095
+#: includes/class-m365-login-admin.php:1154
msgid "Button-only mode"
msgstr "Nur-Button-Modus"
-#: includes/class-m365-login-admin.php:1096
-msgid "Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every interactive password sign-in on the site, including custom login forms. Application passwords, REST, XML-RPC and WP-CLI are not affected."
-msgstr "Blendet die Benutzername/Passwort-Felder aus (auf wp-login.php und in wp_login_form()-Formularen) und lehnt jede interaktive Passwort-Anmeldung auf der Website ab, auch in eigenen Login-Formularen. Anwendungspasswörter, REST, XML-RPC und WP-CLI sind nicht betroffen."
+#: includes/class-m365-login-admin.php:1155
+msgid "Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every sign-in with a normal password on the site – custom login forms, XML-RPC and login endpoints of other plugins included. Application passwords (REST, XML-RPC) and WP-CLI keep working; API requests never receive a login cookie."
+msgstr "Blendet die Felder für Benutzername/Passwort aus (auf wp-login.php und in wp_login_form()-Formularen) und lehnt jede Anmeldung mit einem normalen Passwort auf der Website ab – auch in eigenen Login-Formularen, über XML-RPC und über Login-Endpunkte anderer Plugins. Anwendungspasswörter (REST, XML-RPC) und WP-CLI funktionieren weiter; API-Anfragen erhalten nie ein Login-Cookie."
-#: includes/class-m365-login-admin.php:1101
+#: includes/class-m365-login-admin.php:1160
msgid "Show only the Microsoft button on the login page"
msgstr "Auf der Login-Seite nur den Microsoft-Button anzeigen"
-#: includes/class-m365-login-admin.php:1102
+#: includes/class-m365-login-admin.php:1161
msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
msgstr "Wird aktiv, sobald die Verbindung eingerichtet ist. Vor dem Aktivieren sicherstellen, dass das eigene Konto sich per Microsoft anmelden kann."
-#: includes/class-m365-login-admin.php:1107
+#: includes/class-m365-login-admin.php:1166
msgid "Fallback link (keep it secret)"
msgstr "Fallback-Link (geheim halten)"
-#: includes/class-m365-login-admin.php:1108
+#: includes/class-m365-login-admin.php:1167
msgid "Opening this link shows the password form again in that browser for 30 minutes and allows password sign-in there. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks."
msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular und kann sich dort mit Passwort anmelden. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
-#: includes/class-m365-login-admin.php:1116
+#: includes/class-m365-login-admin.php:1175
msgid "Generate a new key when saving"
msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
-#: includes/class-m365-login-admin.php:1119
+#: includes/class-m365-login-admin.php:1178
msgid "A key is generated automatically the first time you save these settings."
msgstr "Beim ersten Speichern dieser Einstellungen wird automatisch ein Schlüssel erzeugt."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:1125
+#: includes/class-m365-login-admin.php:1184
msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
msgstr "Notschalter: %s in die wp-config.php eintragen, um den Nur-Button-Modus vollständig abzuschalten."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:1134
+#: includes/class-m365-login-admin.php:1193
msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung"
-#: includes/class-m365-login-admin.php:1136
+#: includes/class-m365-login-admin.php:1195
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) – Tokens laufen nie durch den Browser."
-#: includes/class-m365-login-admin.php:1137
+#: includes/class-m365-login-admin.php:1196
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
-#: includes/class-m365-login-admin.php:1138
+#: includes/class-m365-login-admin.php:1197
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr "Signatur des ID-Tokens wird gegen Microsofts veröffentlichte Signaturschlüssel geprüft; Issuer, Audience, Tenant, Ablauf und Nonce werden kontrolliert."
-#: includes/class-m365-login-admin.php:1139
+#: includes/class-m365-login-admin.php:1198
msgid "Client secret encrypted at rest; sign-in never creates accounts or changes passwords."
msgstr "Client Secret verschlüsselt gespeichert; die Anmeldung legt nie Konten an und ändert keine Passwörter."
-#: includes/class-m365-login-admin.php:1147
+#: includes/class-m365-login-admin.php:1206
msgid "Save changes"
msgstr "Änderungen speichern"
-#: includes/class-m365-login-admin.php:1153
+#: includes/class-m365-login-admin.php:1212
msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)"
-#: includes/class-m365-login-admin.php:1154
+#: includes/class-m365-login-admin.php:1213
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
-#: includes/class-m365-login-admin.php:1160
+#: includes/class-m365-login-admin.php:1219
msgid "Plain permalinks are active, so the callback uses a query string. If you enable pretty permalinks later, the redirect URI changes and must be updated in Entra ID."
msgstr "Einfache Permalinks sind aktiv, daher verwendet der Callback einen Query-String. Werden später sprechende Permalinks aktiviert, ändert sich die Umleitungs-URI und muss in Entra ID angepasst werden."
-#: includes/class-m365-login-admin.php:1163
+#: includes/class-m365-login-admin.php:1222
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr "Diese Website nutzt kein HTTPS. Microsoft akzeptiert http://-Umleitungs-URIs nur für localhost; produktive Websites benötigen HTTPS."
-#: includes/class-m365-login-admin.php:1168
+#: includes/class-m365-login-admin.php:1227
msgid "Setup guide: app registration"
msgstr "Anleitung: App-Registrierung"
-#: includes/class-m365-login-admin.php:1170
+#: includes/class-m365-login-admin.php:1229
msgid "Open entra.microsoft.com → Identity → Applications → App registrations → New registration."
msgstr "entra.microsoft.com → Identität → Anwendungen → App-Registrierungen → Neue Registrierung öffnen."
-#: includes/class-m365-login-admin.php:1171
+#: includes/class-m365-login-admin.php:1230
msgid "Name: e.g. \"WordPress login\". Supported account types: \"Accounts in this organizational directory only\" (single tenant)."
msgstr "Name: z. B. „WordPress Login“. Unterstützte Kontotypen: „Nur Konten in diesem Organisationsverzeichnis“ (Single Tenant)."
-#: includes/class-m365-login-admin.php:1172
+#: includes/class-m365-login-admin.php:1231
msgid "Redirect URI: choose the platform Web and paste the URI shown above. Then click Register."
msgstr "Umleitungs-URI: Plattform „Web“ wählen und die oben angezeigte URI einfügen. Dann auf „Registrieren“ klicken."
-#: includes/class-m365-login-admin.php:1173
+#: includes/class-m365-login-admin.php:1232
msgid "On the Overview page copy the Application (client) ID and the Directory (tenant) ID into the Connection tab."
msgstr "Auf der Übersichtsseite die Anwendungs-ID (Client) und die Verzeichnis-ID (Mandant) in den Tab „Verbindung“ kopieren."
-#: includes/class-m365-login-admin.php:1174
+#: includes/class-m365-login-admin.php:1233
msgid "Authentication: leave \"ID tokens\" unchecked (the plugin uses the authorization code flow) and \"Allow public client flows\" on No."
msgstr "Authentifizierung: „ID-Token“ nicht anhaken (das Plugin nutzt den Authorization Code Flow) und „Öffentliche Clientflows zulassen“ auf „Nein“ lassen."
-#: includes/class-m365-login-admin.php:1175
+#: includes/class-m365-login-admin.php:1234
msgid "Token configuration → Add optional claim → ID → tick \"email\" → Add. Confirm the API permission prompt."
msgstr "Tokenkonfiguration → Optionalen Anspruch hinzufügen → ID → „email“ anhaken → Hinzufügen. Die Rückfrage zur API-Berechtigung bestätigen."
-#: includes/class-m365-login-admin.php:1176
+#: includes/class-m365-login-admin.php:1235
msgid "Pick the authentication method on the Connection tab and follow its step-by-step guide (client secret or certificate)."
msgstr "Im Tab „Verbindung“ die Authentifizierungsmethode wählen und der zugehörigen Schritt-für-Schritt-Anleitung folgen (Client Secret oder Zertifikat)."
-#: includes/class-m365-login-admin.php:1177
+#: includes/class-m365-login-admin.php:1236
msgid "Optional: restrict who may use the app under Enterprise applications → your app → Properties → \"Assignment required\" = Yes, then assign users/groups."
msgstr "Optional: Unter Unternehmensanwendungen → deine App → Eigenschaften → „Zuweisung erforderlich“ = Ja einschränken, wer die App nutzen darf, und dann Benutzer/Gruppen zuweisen."
-#: includes/class-m365-login-admin.php:1179
+#: includes/class-m365-login-admin.php:1238
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) – standardmäßig vorhanden."
-#: includes/class-m365-login-admin.php:1180
+#: includes/class-m365-login-admin.php:1239
msgid "Optional, for group restrictions and the user sync: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
msgstr "Optional für Gruppen-Beschränkungen und den Benutzer-Sync: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
-#: includes/class-m365-login-admin.php:1184
+#: includes/class-m365-login-admin.php:1243
msgid "Shortcode"
msgstr "Shortcode"
-#: includes/class-m365-login-admin.php:1185
+#: includes/class-m365-login-admin.php:1244
msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:"
-#: includes/class-m365-login-admin.php:1187
+#: includes/class-m365-login-admin.php:1246
msgid "More options on the Button tab under \"Custom login page\"."
msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“."
-#: includes/class-m365-login-auth.php:173
+#: includes/class-m365-login-auth.php:182
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr "Die Anmeldung mit Passwort ist auf dieser Website deaktiviert. Bitte den Microsoft-Button verwenden."
-#: includes/class-m365-login-auth.php:906
+#: includes/class-m365-login-auth.php:998
+msgid "Microsoft sign-in is temporarily unavailable. Please contact an administrator."
+msgstr "Die Microsoft-Anmeldung ist vorübergehend nicht verfügbar. Bitte wende dich an einen Administrator."
+
+#: includes/class-m365-login-auth.php:1005
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr "Die Passwort-Anmeldung ist für diesen Browser vorübergehend aktiviert (30 Minuten)."
-#: includes/class-m365-login-auth.php:927 includes/class-m365-login-graph.php:63
+#: includes/class-m365-login-auth.php:1026 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
-#: includes/class-m365-login-auth.php:928
+#: includes/class-m365-login-auth.php:1027
msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:929
+#: includes/class-m365-login-auth.php:1028
msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
-#: includes/class-m365-login-auth.php:930
+#: includes/class-m365-login-auth.php:1029
msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:931
+#: includes/class-m365-login-auth.php:1030
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:932
+#: includes/class-m365-login-auth.php:1031
msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
-#: includes/class-m365-login-auth.php:933
+#: includes/class-m365-login-auth.php:1032
msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
-#: includes/class-m365-login-auth.php:934
+#: includes/class-m365-login-auth.php:1033
msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
-#: includes/class-m365-login-auth.php:935
+#: includes/class-m365-login-auth.php:1034
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
-#: includes/class-m365-login-auth.php:936
+#: includes/class-m365-login-auth.php:1035
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:937
+#: includes/class-m365-login-auth.php:1036
msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
-#: includes/class-m365-login-auth.php:938
+#: includes/class-m365-login-auth.php:1037
msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
msgstr "Das Microsoft-Konto ist in keiner Gruppe, die sich hier anmelden darf."
-#: includes/class-m365-login-auth.php:939
+#: includes/class-m365-login-auth.php:1038
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr "Die Gruppenmitgliedschaft konnte nicht geprüft werden. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:940
+#: includes/class-m365-login-auth.php:1039
msgid "Your Microsoft account is a member of a group that is not allowed to sign in here."
msgstr "Dein Microsoft-Konto ist Mitglied einer Gruppe, die sich hier nicht anmelden darf."
-#: includes/class-m365-login-auth.php:941
+#: includes/class-m365-login-auth.php:1040
msgid "The fallback key is not valid."
msgstr "Der Fallback-Schlüssel ist ungültig."
-#: includes/class-m365-login-auth.php:942
+#: includes/class-m365-login-auth.php:1041
msgid "Too many attempts. Please wait 15 minutes."
msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
-#: includes/class-m365-login-auth.php:943
+#: includes/class-m365-login-auth.php:1042
msgid "Too many sign-in attempts from your connection. Please wait a few minutes and try again."
msgstr "Zu viele Anmeldeversuche von dieser Verbindung. Bitte ein paar Minuten warten und erneut versuchen."
-#: includes/class-m365-login-auth.php:944 includes/class-m365-login-sync.php:1520
+#: includes/class-m365-login-auth.php:1043 includes/class-m365-login-sync.php:1684
msgid "This account has been deactivated."
msgstr "Dieses Konto wurde deaktiviert."
+#: includes/class-m365-login-auth.php:1044
+msgid "For security reasons this administrator account can only be linked to a Microsoft account whose user principal name equals the WordPress e-mail address. Please contact an administrator."
+msgstr "Aus Sicherheitsgründen kann dieses Administrator-Konto nur mit einem Microsoft-Konto verknüpft werden, dessen Benutzerprinzipalname der WordPress-E-Mail-Adresse entspricht. Bitte wende dich an einen Administrator."
+
+#: includes/class-m365-login-auth.php:1045
+msgid "Guest and external accounts cannot sign in here."
+msgstr "Gast- und externe Konten können sich hier nicht anmelden."
+
#: includes/class-m365-login-certificate.php:29
msgid "The PHP OpenSSL extension is not available."
msgstr "Die PHP-Erweiterung OpenSSL ist nicht verfügbar."
@@ -1183,30 +1211,38 @@ msgstr "Es werden nur RSA-Schlüssel unterstützt."
msgid "The RSA key must have at least 2048 bits."
msgstr "Der RSA-Schlüssel muss mindestens 2048 Bit haben."
-#: includes/class-m365-login-certificate.php:102
+#: includes/class-m365-login-certificate.php:101
+msgid "The certificate field contains a private key. Paste only the certificate (-----BEGIN CERTIFICATE-----) there."
+msgstr "Das Zertifikatsfeld enthält einen privaten Schlüssel. Füge dort nur das Zertifikat ein (-----BEGIN CERTIFICATE-----)."
+
+#: includes/class-m365-login-certificate.php:105 includes/class-m365-login-certificate.php:119
msgid "The certificate could not be read. Paste it in PEM format (-----BEGIN CERTIFICATE-----)."
msgstr "Das Zertifikat konnte nicht gelesen werden. Bitte im PEM-Format einfügen (-----BEGIN CERTIFICATE-----)."
-#: includes/class-m365-login-certificate.php:105
+#: includes/class-m365-login-certificate.php:108
msgid "The certificate does not belong to this private key."
msgstr "Das Zertifikat gehört nicht zu diesem privaten Schlüssel."
-#: includes/class-m365-login-certificate.php:110
+#: includes/class-m365-login-certificate.php:113
msgid "The certificate has already expired."
msgstr "Das Zertifikat ist bereits abgelaufen."
-#: includes/class-m365-login-graph.php:434
+#: includes/class-m365-login-graph.php:436
msgid "Group"
msgstr "Gruppe"
-#: includes/class-m365-login-graph.php:436
+#: includes/class-m365-login-graph.php:438
msgid "Security group"
msgstr "Sicherheitsgruppe"
-#: includes/class-m365-login-graph.php:438
+#: includes/class-m365-login-graph.php:440
msgid "Microsoft 365 group"
msgstr "Microsoft 365-Gruppe"
+#: includes/class-m365-login-graph.php:444
+msgid "Public Microsoft 365 group – anyone in the organisation can join"
+msgstr "Öffentliche Microsoft-365-Gruppe – jeder in der Organisation kann beitreten"
+
#: includes/class-m365-login-settings.php:55
msgid "Sign in with Microsoft"
msgstr "Login mit Microsoft"
@@ -1215,352 +1251,370 @@ msgstr "Login mit Microsoft"
msgid "or"
msgstr "oder"
-#: includes/class-m365-login-settings.php:226 includes/class-m365-login-settings.php:540
+#: includes/class-m365-login-settings.php:226 includes/class-m365-login-settings.php:543
msgid "The private key could not be encrypted. Is the OpenSSL extension available?"
msgstr "Der private Schlüssel konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login-settings.php:486
+#: includes/class-m365-login-settings.php:489
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
-#: includes/class-m365-login-settings.php:494
+#: includes/class-m365-login-settings.php:497
msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
-#: includes/class-m365-login-settings.php:506
+#: includes/class-m365-login-settings.php:509
msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen."
-#: includes/class-m365-login-settings.php:510
+#: includes/class-m365-login-settings.php:513
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login-settings.php:530
+#: includes/class-m365-login-settings.php:533
msgid "Please paste both the private key and the certificate."
msgstr "Bitte sowohl den privaten Schlüssel als auch das Zertifikat einfügen."
-#: includes/class-m365-login-settings.php:532
+#: includes/class-m365-login-settings.php:535
msgid "The pasted key or certificate is too large."
msgstr "Der eingefügte Schlüssel oder das Zertifikat ist zu groß."
-#: includes/class-m365-login-settings.php:549
+#: includes/class-m365-login-settings.php:552
msgid "Certificate authentication is selected but no certificate is stored yet. Generate one or paste your own; the Microsoft button stays hidden until then."
msgstr "Zertifikats-Authentifizierung ist ausgewählt, aber es ist noch kein Zertifikat gespeichert. Eines erzeugen oder ein eigenes einfügen; bis dahin bleibt der Microsoft-Button ausgeblendet."
-#: includes/class-m365-login-settings.php:582
+#: includes/class-m365-login-settings.php:585
msgid "The custom login page must be a URL on this site."
msgstr "Die eigene Login-Seite muss eine URL dieser Website sein."
-#: includes/class-m365-login-settings.php:689
+#: includes/class-m365-login-settings.php:692
msgid "User sync: \"Delete\" needs a user who receives the posts of deleted accounts. Until one is selected, accounts are deactivated instead."
msgstr "Benutzer-Sync: „Löschen“ braucht einen Benutzer, der die Beiträge gelöschter Konten übernimmt. Bis einer ausgewählt ist, werden Konten stattdessen deaktiviert."
-#: includes/class-m365-login-sync.php:110
+#: includes/class-m365-login-sync.php:119
msgid "Display name"
msgstr "Anzeigename"
-#: includes/class-m365-login-sync.php:114
+#: includes/class-m365-login-sync.php:123
msgid "First name"
msgstr "Vorname"
-#: includes/class-m365-login-sync.php:118
+#: includes/class-m365-login-sync.php:127
msgid "Last name"
msgstr "Nachname"
-#: includes/class-m365-login-sync.php:122
+#: includes/class-m365-login-sync.php:131
msgid "Profile photo (used as avatar)"
msgstr "Profilbild (als Avatar)"
-#: includes/class-m365-login-sync.php:126
+#: includes/class-m365-login-sync.php:135
msgid "Job title"
msgstr "Position"
-#: includes/class-m365-login-sync.php:130
+#: includes/class-m365-login-sync.php:139
msgid "Department"
msgstr "Abteilung"
-#: includes/class-m365-login-sync.php:134
+#: includes/class-m365-login-sync.php:143
msgid "Company"
msgstr "Firma"
-#: includes/class-m365-login-sync.php:138
+#: includes/class-m365-login-sync.php:147
msgid "Office"
msgstr "Büro"
-#: includes/class-m365-login-sync.php:142
+#: includes/class-m365-login-sync.php:151
msgid "Employee ID"
msgstr "Personalnummer"
-#: includes/class-m365-login-sync.php:146
+#: includes/class-m365-login-sync.php:155
msgid "Business phone"
msgstr "Telefon (geschäftlich)"
-#: includes/class-m365-login-sync.php:150
+#: includes/class-m365-login-sync.php:159
msgid "Mobile phone"
msgstr "Mobiltelefon"
-#: includes/class-m365-login-sync.php:154
+#: includes/class-m365-login-sync.php:163
msgid "Street address"
msgstr "Straße"
-#: includes/class-m365-login-sync.php:158
+#: includes/class-m365-login-sync.php:167
msgid "Postal code"
msgstr "Postleitzahl"
-#: includes/class-m365-login-sync.php:162
+#: includes/class-m365-login-sync.php:171
msgid "City"
msgstr "Ort"
-#: includes/class-m365-login-sync.php:166
+#: includes/class-m365-login-sync.php:175
msgid "State / province"
msgstr "Bundesland / Region"
-#: includes/class-m365-login-sync.php:170
+#: includes/class-m365-login-sync.php:179
msgid "Country"
msgstr "Land"
-#: includes/class-m365-login-sync.php:174
+#: includes/class-m365-login-sync.php:183
msgid "Language (sets the admin language if installed)"
msgstr "Sprache (setzt die Backend-Sprache, falls installiert)"
-#: includes/class-m365-login-sync.php:309
+#: includes/class-m365-login-sync.php:319
msgid "Another sync is still running. Please try again in a few minutes."
msgstr "Ein anderer Sync läuft noch. Bitte versuche es in ein paar Minuten erneut."
-#: includes/class-m365-login-sync.php:366
+#: includes/class-m365-login-sync.php:405
msgid "The connection to Microsoft Entra ID is not configured yet."
msgstr "Die Verbindung zu Microsoft Entra ID ist noch nicht eingerichtet."
-#: includes/class-m365-login-sync.php:370
+#: includes/class-m365-login-sync.php:409
msgid "The user sync needs a pinned tenant ID (GUID) on the Connection tab."
msgstr "Der Benutzer-Sync braucht eine feste Tenant-ID (GUID) im Tab „Verbindung“."
-#: includes/class-m365-login-sync.php:374
+#: includes/class-m365-login-sync.php:413
msgid "The default role does not exist. Please check the sync settings."
msgstr "Die Standardrolle existiert nicht. Bitte prüfe die Sync-Einstellungen."
#. translators: %d: number of users
-#: includes/class-m365-login-sync.php:386
+#: includes/class-m365-login-sync.php:425
msgid "%d user read from Microsoft 365."
msgid_plural "%d users read from Microsoft 365."
msgstr[0] "%d Benutzer aus Microsoft 365 gelesen."
msgstr[1] "%d Benutzer aus Microsoft 365 gelesen."
+#: includes/class-m365-login-sync.php:436
+msgid "Microsoft 365 returned no users at all while accounts are linked. Nothing was changed. Check the tenant and the sync groups."
+msgstr "Microsoft 365 hat überhaupt keine Benutzer geliefert, obwohl Konten verknüpft sind. Es wurde nichts geändert. Prüfe den Tenant und die Sync-Gruppen."
+
+#: includes/class-m365-login-sync.php:484
+msgid "The tenant ID changed since the last sync. Accounts linked in the old tenant are not found in the new one, so no account was deactivated or deleted in this run. Run the sync again to apply deactivations."
+msgstr "Die Tenant-ID hat sich seit dem letzten Sync geändert. Im alten Tenant verknüpfte Konten werden im neuen nicht gefunden, deshalb wurde in diesem Lauf kein Konto deaktiviert oder gelöscht. Starte den Sync erneut, um Deaktivierungen anzuwenden."
+
#. translators: 1: number of accounts, 2: limit
-#: includes/class-m365-login-sync.php:443
+#: includes/class-m365-login-sync.php:496
msgid "Safety stop: %1$d accounts would be deactivated or deleted, more than the limit of %2$d per run. No account was deactivated or deleted. Check the sync groups and the tenant, then run the sync again (the limit can be changed with the m365_login_sync_deprovision_limit filter)."
msgstr "Sicherheitsstopp: %1$d Konten würden deaktiviert oder gelöscht, mehr als das Limit von %2$d pro Lauf. Es wurde kein Konto deaktiviert oder gelöscht. Prüfe die Sync-Gruppen und den Tenant und starte den Sync dann erneut (das Limit lässt sich mit dem Filter m365_login_sync_deprovision_limit ändern)."
+#: includes/class-m365-login-sync.php:621 includes/class-m365-login-sync.php:674 includes/class-m365-login-sync.php:1028 includes/class-m365-login-sync.php:1816
+msgid "disabled in Microsoft 365"
+msgstr "in Microsoft 365 deaktiviert"
+
#. translators: %s: user principal name
-#: includes/class-m365-login-sync.php:567
+#: includes/class-m365-login-sync.php:632
msgid "%s: no usable e-mail address, skipped."
msgstr "%s: keine verwendbare E-Mail-Adresse, übersprungen."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:574
+#: includes/class-m365-login-sync.php:639
msgid "%s: e-mail domain is not on the allow-list, skipped."
msgstr "%s: E-Mail-Domain steht nicht auf der Liste erlaubter Domains, übersprungen."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:586
+#: includes/class-m365-login-sync.php:651
msgid "%s: the WordPress account with this e-mail address is linked to a different Microsoft account, skipped."
msgstr "%s: Das WordPress-Konto mit dieser E-Mail-Adresse ist mit einem anderen Microsoft-Konto verknüpft, übersprungen."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:590
+#: includes/class-m365-login-sync.php:656
+msgid "%s: privileged WordPress account – it is only linked when the Microsoft user principal name equals its e-mail address (member account, no guest). Skipped."
+msgstr "%s: privilegiertes WordPress-Konto – es wird nur verknüpft, wenn der Microsoft-Benutzerprinzipalname seiner E-Mail-Adresse entspricht (Mitgliedskonto, kein Gast). Übersprungen."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:660
msgid "%s: existing account linked."
msgstr "%s: bestehendes Konto verknüpft."
-#: includes/class-m365-login-sync.php:604 includes/class-m365-login-sync.php:955 includes/class-m365-login-sync.php:1652
-msgid "disabled in Microsoft 365"
-msgstr "in Microsoft 365 deaktiviert"
-
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:615
+#: includes/class-m365-login-sync.php:685
msgid "%s: added to this site."
msgstr "%s: zu dieser Website hinzugefügt."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:624
+#: includes/class-m365-login-sync.php:694
msgid "%s: reactivated (active in Microsoft 365 again)."
msgstr "%s: reaktiviert (in Microsoft 365 wieder aktiv)."
#. translators: 1: e-mail address, 2: list of changed fields
-#: includes/class-m365-login-sync.php:635
+#: includes/class-m365-login-sync.php:705
msgid "%1$s: updated (%2$s)."
msgstr "%1$s: aktualisiert (%2$s)."
#. translators: 1: e-mail address, 2: role names
-#: includes/class-m365-login-sync.php:661
+#: includes/class-m365-login-sync.php:731
msgid "%1$s: account created (%2$s)."
msgstr "%1$s: Konto angelegt (%2$s)."
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:688
+#: includes/class-m365-login-sync.php:758
msgid "%1$s: account could not be created: %2$s"
msgstr "%1$s: Konto konnte nicht angelegt werden: %2$s"
+#. translators: 1: current e-mail address, 2: e-mail address in Microsoft 365
+#: includes/class-m365-login-sync.php:816
+msgid "%1$s: the e-mail address in Microsoft 365 changed to %2$s. It is not changed automatically for privileged accounts – update it by hand if intended."
+msgstr "%1$s: Die E-Mail-Adresse in Microsoft 365 wurde zu %2$s geändert. Bei privilegierten Konten wird sie nicht automatisch geändert – bei Bedarf von Hand anpassen."
+
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:746
+#: includes/class-m365-login-sync.php:819
msgid "%s: e-mail address is used by another WordPress account and was not changed."
msgstr "%s: Die E-Mail-Adresse gehört bereits einem anderen WordPress-Konto und wurde nicht geändert."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:749
+#: includes/class-m365-login-sync.php:822
msgid "e-mail"
msgstr "E-Mail"
#. translators: %s: profile field
-#: includes/class-m365-login-sync.php:799
+#: includes/class-m365-login-sync.php:872
msgid "%s removed"
msgstr "%s entfernt"
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:809
+#: includes/class-m365-login-sync.php:882
msgid "%1$s: profile could not be updated: %2$s"
msgstr "%1$s: Profil konnte nicht aktualisiert werden: %2$s"
#. translators: %s: role names
-#: includes/class-m365-login-sync.php:915
+#: includes/class-m365-login-sync.php:988
msgid "roles: %s"
msgstr "Rollen: %s"
-#: includes/class-m365-login-sync.php:950 includes/class-m365-login-sync.php:1653
+#: includes/class-m365-login-sync.php:1023 includes/class-m365-login-sync.php:1817
msgid "deleted in Microsoft 365"
msgstr "in Microsoft 365 gelöscht"
-#: includes/class-m365-login-sync.php:958 includes/class-m365-login-sync.php:1654
+#: includes/class-m365-login-sync.php:1031 includes/class-m365-login-sync.php:1818
msgid "no longer a member of the sync groups"
msgstr "kein Mitglied der Sync-Gruppen mehr"
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:978
+#: includes/class-m365-login-sync.php:1063
msgid "%1$s: %2$s, but the account is protected (administrator or your own account) and was not changed."
msgstr "%1$s: %2$s, das Konto ist aber geschützt (Administrator oder dein eigenes Konto) und wurde nicht geändert."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1011
+#: includes/class-m365-login-sync.php:1140
msgid "%s: no valid user to receive the content is selected, so the account is deactivated instead of deleted."
msgstr "%s: Es ist kein gültiger Benutzer für die Übernahme der Inhalte ausgewählt, deshalb wird das Konto deaktiviert statt gelöscht."
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:1020
+#: includes/class-m365-login-sync.php:1149
msgid "%1$s: account deleted (%2$s)."
msgstr "%1$s: Konto gelöscht (%2$s)."
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:1029
+#: includes/class-m365-login-sync.php:1158
msgid "%1$s: account deactivated (%2$s)."
msgstr "%1$s: Konto deaktiviert (%2$s)."
-#: includes/class-m365-login-sync.php:1108
+#: includes/class-m365-login-sync.php:1237
msgid "Microsoft Graph refused the request. Grant the application permissions \"User.Read.All\" and \"GroupMember.Read.All\" with admin consent in Entra ID."
msgstr "Microsoft Graph hat die Anfrage abgelehnt. Erteile in Entra ID die Anwendungsberechtigungen „User.Read.All“ und „GroupMember.Read.All“ mit Administratorzustimmung."
#. translators: %s: error message
-#: includes/class-m365-login-sync.php:1111
+#: includes/class-m365-login-sync.php:1240
msgid "Microsoft Graph error: %s"
msgstr "Microsoft-Graph-Fehler: %s"
-#: includes/class-m365-login-sync.php:1129
+#: includes/class-m365-login-sync.php:1258
msgid "Log truncated."
msgstr "Protokoll gekürzt."
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:1205
+#: includes/class-m365-login-sync.php:1334
msgid "%1$s: profile photo could not be read: %2$s"
msgstr "%1$s: Profilbild konnte nicht gelesen werden: %2$s"
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1229 includes/class-m365-login-sync.php:1272
+#: includes/class-m365-login-sync.php:1358 includes/class-m365-login-sync.php:1401
msgid "%s: profile photo updated."
msgstr "%s: Profilbild aktualisiert."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1248
+#: includes/class-m365-login-sync.php:1377
msgid "%s: profile photo could not be downloaded."
msgstr "%s: Profilbild konnte nicht heruntergeladen werden."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1255
+#: includes/class-m365-login-sync.php:1384
msgid "%s: profile photo is not a valid image or could not be saved."
msgstr "%s: Profilbild ist kein gültiges Bild oder konnte nicht gespeichert werden."
#. translators: %d: number of photos
-#: includes/class-m365-login-sync.php:1278
+#: includes/class-m365-login-sync.php:1407
msgid "%d changed profile photo will be downloaded in the next run (download limit per run reached)."
msgid_plural "%d changed profile photos will be downloaded in the next run (download limit per run reached)."
msgstr[0] "%d geändertes Profilbild wird im nächsten Lauf heruntergeladen (Download-Limit pro Lauf erreicht)."
msgstr[1] "%d geänderte Profilbilder werden im nächsten Lauf heruntergeladen (Download-Limit pro Lauf erreicht)."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1293
+#: includes/class-m365-login-sync.php:1422
msgid "%s: profile photo removed."
msgstr "%s: Profilbild entfernt."
#. translators: %d: number of photos
-#: includes/class-m365-login-sync.php:1321
+#: includes/class-m365-login-sync.php:1450
msgid "Profile photo sync is off: %d stored photo removed."
msgid_plural "Profile photo sync is off: %d stored photos removed."
msgstr[0] "Profilbild-Sync ist aus: %d gespeichertes Profilbild entfernt."
msgstr[1] "Profilbild-Sync ist aus: %d gespeicherte Profilbilder entfernt."
-#: includes/class-m365-login-sync.php:1549 includes/class-m365-login-sync.php:1682
+#: includes/class-m365-login-sync.php:1713 includes/class-m365-login-sync.php:1846
msgid "Microsoft 365"
msgstr "Microsoft 365"
-#: includes/class-m365-login-sync.php:1567
+#: includes/class-m365-login-sync.php:1731
msgid "Deactivated"
msgstr "Deaktiviert"
-#: includes/class-m365-login-sync.php:1570
+#: includes/class-m365-login-sync.php:1734
msgid "Imported"
msgstr "Importiert"
-#: includes/class-m365-login-sync.php:1572
+#: includes/class-m365-login-sync.php:1736
msgid "Linked"
msgstr "Verknüpft"
-#: includes/class-m365-login-sync.php:1600
+#: includes/class-m365-login-sync.php:1764
msgid "Reactivate"
msgstr "Reaktivieren"
-#: includes/class-m365-login-sync.php:1600
+#: includes/class-m365-login-sync.php:1764
msgid "Deactivate"
msgstr "Deaktivieren"
-#: includes/class-m365-login-sync.php:1633
+#: includes/class-m365-login-sync.php:1797
msgid "The account has been deactivated and signed out everywhere."
msgstr "Das Konto wurde deaktiviert und überall abgemeldet."
-#: includes/class-m365-login-sync.php:1634
+#: includes/class-m365-login-sync.php:1798
msgid "The account has been reactivated."
msgstr "Das Konto wurde reaktiviert."
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1658
+#: includes/class-m365-login-sync.php:1822
msgid "Deactivated since %1$s (%2$s)"
msgstr "Deaktiviert seit %1$s (%2$s)"
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1660
+#: includes/class-m365-login-sync.php:1824
msgid "manually"
msgstr "manuell"
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1662
+#: includes/class-m365-login-sync.php:1826
msgid "Status"
msgstr "Status"
-#: includes/class-m365-login-sync.php:1665
+#: includes/class-m365-login-sync.php:1829
msgid "Object ID"
msgstr "Objekt-ID"
-#: includes/class-m365-login-sync.php:1669
+#: includes/class-m365-login-sync.php:1833
msgid "Last sync"
msgstr "Letzter Sync"
-#: includes/class-m365-login-sync.php:1691
+#: includes/class-m365-login-sync.php:1855
msgid "These values are managed by the Microsoft 365 user sync and overwritten on the next run."
msgstr "Diese Werte verwaltet der Microsoft-365-Benutzer-Sync; sie werden beim nächsten Lauf überschrieben."
diff --git a/languages/m365-login-de_DE_formal.mo b/languages/m365-login-de_DE_formal.mo
index dcc1421..8beaf31 100644
Binary files a/languages/m365-login-de_DE_formal.mo and b/languages/m365-login-de_DE_formal.mo differ
diff --git a/languages/m365-login-de_DE_formal.po b/languages/m365-login-de_DE_formal.po
index 7ec2925..4f71d7e 100644
--- a/languages/m365-login-de_DE_formal.po
+++ b/languages/m365-login-de_DE_formal.po
@@ -16,1133 +16,1161 @@ msgstr ""
"X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n"
-#: includes/class-m365-login-admin.php:92 includes/class-m365-login-admin.php:93 includes/class-m365-login-admin.php:104 includes/class-m365-login-admin.php:725
+#: includes/class-m365-login-admin.php:108 includes/class-m365-login-admin.php:109 includes/class-m365-login-admin.php:120 includes/class-m365-login-admin.php:778
msgid "M365 Login"
msgstr "M365 Login"
-#: includes/class-m365-login-admin.php:119
+#: includes/class-m365-login-admin.php:135
msgid "Connection"
msgstr "Verbindung"
-#: includes/class-m365-login-admin.php:120
+#: includes/class-m365-login-admin.php:136
msgid "Button"
msgstr "Button"
-#: includes/class-m365-login-admin.php:121
+#: includes/class-m365-login-admin.php:137
msgid "Security"
msgstr "Sicherheit"
-#: includes/class-m365-login-admin.php:122
+#: includes/class-m365-login-admin.php:138
msgid "User sync"
msgstr "Benutzer-Sync"
-#: includes/class-m365-login-admin.php:197
-msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
-msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
+#: includes/class-m365-login-admin.php:207
+msgid "M365 Login: button-only mode is on, but the connection to Microsoft is broken (missing or undecryptable secret, or expired certificate). Nobody can sign in except through the fallback link."
+msgstr "M365 Login: Der Nur-Button-Modus ist aktiv, aber die Verbindung zu Microsoft ist gestört (Secret fehlt oder ist nicht entschlüsselbar, oder das Zertifikat ist abgelaufen). Anmelden ist nur noch über den Fallback-Link möglich."
-#: includes/class-m365-login-admin.php:199
+#: includes/class-m365-login-admin.php:209 includes/class-m365-login-admin.php:227
msgid "Open the settings"
msgstr "Einstellungen öffnen"
-#: includes/class-m365-login-admin.php:230
+#: includes/class-m365-login-admin.php:225
+msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
+msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
+
+#: includes/class-m365-login-admin.php:263
msgid "Choose button icon"
msgstr "Button-Icon auswählen"
-#: includes/class-m365-login-admin.php:231
+#: includes/class-m365-login-admin.php:264
msgid "Use this icon"
msgstr "Dieses Icon verwenden"
-#: includes/class-m365-login-admin.php:232
+#: includes/class-m365-login-admin.php:265
msgid "Copied!"
msgstr "Kopiert!"
-#: includes/class-m365-login-admin.php:233 includes/class-m365-login-admin.php:848 includes/class-m365-login-admin.php:1112 includes/class-m365-login-admin.php:1157
+#: includes/class-m365-login-admin.php:266 includes/class-m365-login-admin.php:904 includes/class-m365-login-admin.php:1171 includes/class-m365-login-admin.php:1216
msgid "Copy"
msgstr "Kopieren"
-#: includes/class-m365-login-admin.php:234
+#: includes/class-m365-login-admin.php:267
msgid "Testing…"
msgstr "Wird geprüft …"
-#: includes/class-m365-login-admin.php:235
+#: includes/class-m365-login-admin.php:268
msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections."
msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen."
-#: includes/class-m365-login-admin.php:236
+#: includes/class-m365-login-admin.php:269
msgid "No groups found."
msgstr "Keine Gruppen gefunden."
-#: includes/class-m365-login-admin.php:237
+#: includes/class-m365-login-admin.php:270
msgid "Searching…"
msgstr "Suche läuft …"
-#: includes/class-m365-login-admin.php:238
+#: includes/class-m365-login-admin.php:271
msgid "Add"
msgstr "Hinzufügen"
-#: includes/class-m365-login-admin.php:239 includes/class-m365-login-admin.php:495
+#: includes/class-m365-login-admin.php:272 includes/class-m365-login-admin.php:528
msgid "Remove"
msgstr "Entfernen"
-#: includes/class-m365-login-admin.php:240 includes/class-m365-login-admin.php:303 includes/class-m365-login-admin.php:468
+#: includes/class-m365-login-admin.php:273 includes/class-m365-login-admin.php:336 includes/class-m365-login-admin.php:501
msgid "Save the connection settings first, then search for groups."
msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen."
-#: includes/class-m365-login-admin.php:241
+#: includes/class-m365-login-admin.php:274
msgid "Generate a new fallback key on save? The old link stops working."
msgstr "Beim Speichern einen neuen Fallback-Schlüssel erzeugen? Der alte Link funktioniert dann nicht mehr."
-#: includes/class-m365-login-admin.php:242
+#: includes/class-m365-login-admin.php:275
msgid "Generating a 3072-bit key pair, this takes a moment…"
msgstr "3072-Bit-Schlüsselpaar wird erzeugt, das dauert einen Moment …"
-#: includes/class-m365-login-admin.php:243
+#: includes/class-m365-login-admin.php:276
msgid "Replace the stored certificate? Sign-in stops working until the new certificate is uploaded to Entra ID."
msgstr "Gespeichertes Zertifikat ersetzen? Die Anmeldung funktioniert erst wieder, wenn das neue Zertifikat in Entra ID hochgeladen ist."
-#: includes/class-m365-login-admin.php:244
+#: includes/class-m365-login-admin.php:277
msgid "Remove the stored certificate when saving? Sign-in with the certificate method stops working."
msgstr "Gespeichertes Zertifikat beim Speichern entfernen? Die Anmeldung per Zertifikat funktioniert dann nicht mehr."
-#: includes/class-m365-login-admin.php:245
+#: includes/class-m365-login-admin.php:278
msgid "Sync is running, this can take a while for large directories…"
msgstr "Sync läuft, bei großen Verzeichnissen kann das etwas dauern …"
-#: includes/class-m365-login-admin.php:246
+#: includes/class-m365-login-admin.php:279
msgid "Run the sync now with the saved settings? Accounts are created, updated and possibly deactivated or deleted. Tip: run a dry run first."
msgstr "Sync jetzt mit den gespeicherten Einstellungen ausführen? Konten werden angelegt, aktualisiert und eventuell deaktiviert oder gelöscht. Tipp: Führen Sie zuerst einen Testlauf aus."
-#: includes/class-m365-login-admin.php:247
+#: includes/class-m365-login-admin.php:280
msgid "The request failed or timed out. Reload the page in a few minutes to see the report; for very large directories use \"wp m365-login sync\" (WP-CLI)."
msgstr "Die Anfrage ist fehlgeschlagen oder hat zu lange gedauert. Laden Sie die Seite in ein paar Minuten neu, um den Bericht zu sehen; für sehr große Verzeichnisse nutzen Sie „wp m365-login sync“ (WP-CLI)."
-#: includes/class-m365-login-admin.php:248
+#: includes/class-m365-login-admin.php:281
msgid "You have unsaved changes. The sync uses the saved settings – save first."
msgstr "Sie haben ungespeicherte Änderungen. Der Sync verwendet die gespeicherten Einstellungen – speichern Sie zuerst."
-#: includes/class-m365-login-admin.php:249 includes/class-m365-login-admin.php:482
+#: includes/class-m365-login-admin.php:282 includes/class-m365-login-admin.php:515
msgid "Move up"
msgstr "Nach oben"
-#: includes/class-m365-login-admin.php:261 includes/class-m365-login-admin.php:300 includes/class-m365-login-admin.php:326 includes/class-m365-login-admin.php:359 includes/class-m365-login-admin.php:514 includes/class-m365-login-sync.php:1611
+#: includes/class-m365-login-admin.php:294 includes/class-m365-login-admin.php:333 includes/class-m365-login-admin.php:359 includes/class-m365-login-admin.php:392 includes/class-m365-login-admin.php:566 includes/class-m365-login-sync.php:1775
msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:266
+#: includes/class-m365-login-admin.php:299
msgid "Please enter a valid tenant ID first."
msgstr "Bitte zuerst eine gültige Tenant-ID eingeben."
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:280
+#: includes/class-m365-login-admin.php:313
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?"
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:289
+#: includes/class-m365-login-admin.php:322
msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen."
-#: includes/class-m365-login-admin.php:312
+#: includes/class-m365-login-admin.php:345
msgid "Microsoft Graph refused the request. Grant the application permission \"GroupMember.Read.All\" (or \"Directory.Read.All\") with admin consent in Entra ID."
msgstr "Microsoft Graph hat die Anfrage abgelehnt. In Entra ID die Anwendungsberechtigung „GroupMember.Read.All“ (oder „Directory.Read.All“) mit Administratorzustimmung erteilen."
-#: includes/class-m365-login-admin.php:330
+#: includes/class-m365-login-admin.php:363
msgid "Unknown operation."
msgstr "Unbekannte Aktion."
-#: includes/class-m365-login-admin.php:347
+#: includes/class-m365-login-admin.php:380
msgid "Certificate generated and stored. Download the .cer file and upload it in Entra ID."
msgstr "Zertifikat erzeugt und gespeichert. Jetzt die .cer-Datei herunterladen und in Entra ID hochladen."
-#: includes/class-m365-login-admin.php:374
+#: includes/class-m365-login-admin.php:407
msgid "The sync has not run yet."
msgstr "Der Sync ist noch nicht gelaufen."
-#: includes/class-m365-login-admin.php:378
+#: includes/class-m365-login-admin.php:411
msgid "Finished"
msgstr "Abgeschlossen"
-#: includes/class-m365-login-admin.php:379
+#: includes/class-m365-login-admin.php:412
msgid "Failed"
msgstr "Fehlgeschlagen"
-#: includes/class-m365-login-admin.php:380
+#: includes/class-m365-login-admin.php:413
msgid "Stopped by the safety limit"
msgstr "Vom Sicherheitslimit gestoppt"
-#: includes/class-m365-login-admin.php:381
+#: includes/class-m365-login-admin.php:414
msgid "Not started"
msgstr "Nicht gestartet"
-#: includes/class-m365-login-admin.php:384
+#: includes/class-m365-login-admin.php:417
msgid "started manually"
msgstr "manuell gestartet"
-#: includes/class-m365-login-admin.php:385
+#: includes/class-m365-login-admin.php:418
msgid "scheduled"
msgstr "geplant"
-#: includes/class-m365-login-admin.php:386
+#: includes/class-m365-login-admin.php:419
msgid "WP-CLI"
msgstr "WP-CLI"
-#: includes/class-m365-login-admin.php:389
+#: includes/class-m365-login-admin.php:422
msgid "would be created"
msgstr "würden angelegt"
-#: includes/class-m365-login-admin.php:389
+#: includes/class-m365-login-admin.php:422
msgid "created"
msgstr "angelegt"
-#: includes/class-m365-login-admin.php:390
+#: includes/class-m365-login-admin.php:423
msgid "would be updated"
msgstr "würden aktualisiert"
-#: includes/class-m365-login-admin.php:390
+#: includes/class-m365-login-admin.php:423
msgid "updated"
msgstr "aktualisiert"
-#: includes/class-m365-login-admin.php:391
+#: includes/class-m365-login-admin.php:424
msgid "would be linked"
msgstr "würden verknüpft"
-#: includes/class-m365-login-admin.php:391
+#: includes/class-m365-login-admin.php:424
msgid "linked"
msgstr "verknüpft"
-#: includes/class-m365-login-admin.php:392
+#: includes/class-m365-login-admin.php:425
msgid "unchanged"
msgstr "unverändert"
-#: includes/class-m365-login-admin.php:393
+#: includes/class-m365-login-admin.php:426
msgid "would be deactivated"
msgstr "würden deaktiviert"
-#: includes/class-m365-login-admin.php:393
+#: includes/class-m365-login-admin.php:426
msgid "deactivated"
msgstr "deaktiviert"
-#: includes/class-m365-login-admin.php:394
+#: includes/class-m365-login-admin.php:427
msgid "would be reactivated"
msgstr "würden reaktiviert"
-#: includes/class-m365-login-admin.php:394
+#: includes/class-m365-login-admin.php:427
msgid "reactivated"
msgstr "reaktiviert"
-#: includes/class-m365-login-admin.php:395
+#: includes/class-m365-login-admin.php:428
msgid "would be deleted"
msgstr "würden gelöscht"
-#: includes/class-m365-login-admin.php:395
+#: includes/class-m365-login-admin.php:428
msgid "deleted"
msgstr "gelöscht"
-#: includes/class-m365-login-admin.php:396
+#: includes/class-m365-login-admin.php:429
msgid "photos"
msgstr "Profilbilder"
-#: includes/class-m365-login-admin.php:397
+#: includes/class-m365-login-admin.php:430
msgid "skipped"
msgstr "übersprungen"
-#: includes/class-m365-login-admin.php:398
+#: includes/class-m365-login-admin.php:431
msgid "errors"
msgstr "Fehler"
-#: includes/class-m365-login-admin.php:411
+#: includes/class-m365-login-admin.php:444
msgid "Dry run – nothing was changed"
msgstr "Testlauf – nichts wurde geändert"
#. translators: 1: date and time, 2: how the run was started, 3: duration in seconds
-#: includes/class-m365-login-admin.php:416
+#: includes/class-m365-login-admin.php:449
msgid "%1$s, %2$s, %3$d s"
msgstr "%1$s, %2$s, %3$d s"
#. translators: %d: number of log entries
-#: includes/class-m365-login-admin.php:434
+#: includes/class-m365-login-admin.php:467
msgid "Log (%d entry)"
msgid_plural "Log (%d entries)"
msgstr[0] "Protokoll (%d Eintrag)"
msgstr[1] "Protokoll (%d Einträge)"
-#: includes/class-m365-login-admin.php:462
+#: includes/class-m365-login-admin.php:495
msgid "Search groups"
msgstr "Gruppen suchen"
-#: includes/class-m365-login-admin.php:464
+#: includes/class-m365-login-admin.php:497
msgid "Type a group name or paste an object ID…"
msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
-#: includes/class-m365-login-admin.php:465
+#: includes/class-m365-login-admin.php:498
msgid "Search"
msgstr "Suchen"
-#: includes/class-m365-login-admin.php:470
+#: includes/class-m365-login-admin.php:503
msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
-#: includes/class-m365-login-admin.php:476
+#: includes/class-m365-login-admin.php:509
msgid "Selected groups"
msgstr "Ausgewählte Gruppen"
-#: includes/class-m365-login-admin.php:488
+#: includes/class-m365-login-admin.php:521
msgid "WordPress role"
msgstr "WordPress-Rolle"
-#: includes/class-m365-login-admin.php:520
+#: includes/class-m365-login-admin.php:572
msgid "No certificate is stored."
msgstr "Es ist kein Zertifikat gespeichert."
-#: includes/class-m365-login-admin.php:545
+#: includes/class-m365-login-admin.php:597
msgid "Do nothing"
msgstr "Nichts tun"
-#: includes/class-m365-login-admin.php:546
+#: includes/class-m365-login-admin.php:598
msgid "Deactivate the WordPress account"
msgstr "WordPress-Konto deaktivieren"
-#: includes/class-m365-login-admin.php:547
+#: includes/class-m365-login-admin.php:599
msgid "Delete the WordPress account"
msgstr "WordPress-Konto löschen"
-#: includes/class-m365-login-admin.php:550
+#: includes/class-m365-login-admin.php:602
msgid "Account disabled in Microsoft 365 (sign-in blocked)"
msgstr "Konto in Microsoft 365 deaktiviert (Anmeldung blockiert)"
-#: includes/class-m365-login-admin.php:551
+#: includes/class-m365-login-admin.php:603
msgid "Account deleted in Microsoft 365"
msgstr "Konto in Microsoft 365 gelöscht"
-#: includes/class-m365-login-admin.php:552
+#: includes/class-m365-login-admin.php:604
msgid "No longer a member of the sync groups"
msgstr "Kein Mitglied der Sync-Gruppen mehr"
-#: includes/class-m365-login-admin.php:557
+#: includes/class-m365-login-admin.php:609
msgid "Import users from Microsoft 365"
msgstr "Benutzer aus Microsoft 365 importieren"
-#: includes/class-m365-login-admin.php:558
+#: includes/class-m365-login-admin.php:610
msgid "Creates a WordPress account for every Microsoft 365 user in scope, links existing accounts by e-mail address, keeps roles and profile fields up to date and deactivates or deletes accounts that were disabled or removed in Microsoft 365. New accounts get a random password and no e-mail; people sign in with the Microsoft button."
msgstr "Legt für jeden Microsoft-365-Benutzer im Geltungsbereich ein WordPress-Konto an, verknüpft bestehende Konten über die E-Mail-Adresse, hält Rollen und Profilfelder aktuell und deaktiviert oder löscht Konten, die in Microsoft 365 deaktiviert oder entfernt wurden. Neue Konten erhalten ein Zufallspasswort und keine E-Mail; die Anmeldung erfolgt über den Microsoft-Button."
-#: includes/class-m365-login-admin.php:563
+#: includes/class-m365-login-admin.php:615
msgid "Run the sync automatically"
msgstr "Sync automatisch ausführen"
-#: includes/class-m365-login-admin.php:564
+#: includes/class-m365-login-admin.php:616
msgid "Uses WP-Cron, which runs when the site receives visits. For exact timing, trigger wp-cron.php from a real cron job or run \"wp m365-login sync\"."
msgstr "Nutzt WP-Cron, das bei Besuchen der Website ausgelöst wird. Für genaue Zeiten rufen Sie wp-cron.php über einen echten Cronjob auf oder führen Sie „wp m365-login sync“ aus."
-#: includes/class-m365-login-admin.php:570
+#: includes/class-m365-login-admin.php:622
msgid "Interval"
msgstr "Intervall"
-#: includes/class-m365-login-admin.php:572
+#: includes/class-m365-login-admin.php:624
msgid "Hourly"
msgstr "Stündlich"
-#: includes/class-m365-login-admin.php:573
+#: includes/class-m365-login-admin.php:625
msgid "Twice daily"
msgstr "Zweimal täglich"
-#: includes/class-m365-login-admin.php:574
+#: includes/class-m365-login-admin.php:626
msgid "Daily"
msgstr "Täglich"
#. translators: %s: date and time
-#: includes/class-m365-login-admin.php:578
+#: includes/class-m365-login-admin.php:630
msgid "Next run: %s"
msgstr "Nächster Lauf: %s"
-#: includes/class-m365-login-admin.php:586
+#: includes/class-m365-login-admin.php:638
msgid "Also import guest users (B2B)"
msgstr "Auch Gastbenutzer importieren (B2B)"
-#: includes/class-m365-login-admin.php:587
+#: includes/class-m365-login-admin.php:639
msgid "Guests are external people invited into your tenant. Off by default."
msgstr "Gäste sind externe Personen, die in Ihren Tenant eingeladen wurden. Standardmäßig aus."
-#: includes/class-m365-login-admin.php:591
+#: includes/class-m365-login-admin.php:643
msgid "Which users? (optional)"
msgstr "Welche Benutzer? (optional)"
-#: includes/class-m365-login-admin.php:592
+#: includes/class-m365-login-admin.php:644
msgid "Limit the import to members of these groups (nested memberships count). Without groups, every user of the tenant is imported. The e-mail domain allow-list on the Security tab applies as well."
msgstr "Beschränkt den Import auf Mitglieder dieser Gruppen (verschachtelte Mitgliedschaften zählen). Ohne Gruppen wird jeder Benutzer des Tenants importiert. Die Liste erlaubter E-Mail-Domains im Tab „Sicherheit“ gilt ebenfalls."
-#: includes/class-m365-login-admin.php:593
+#: includes/class-m365-login-admin.php:645
msgid "No groups selected – all users of the tenant are imported."
msgstr "Keine Gruppen ausgewählt – alle Benutzer des Tenants werden importiert."
-#: includes/class-m365-login-admin.php:597
+#: includes/class-m365-login-admin.php:649
msgid "Roles"
msgstr "Rollen"
-#: includes/class-m365-login-admin.php:600
+#: includes/class-m365-login-admin.php:652
msgid "Default role"
msgstr "Standardrolle"
-#: includes/class-m365-login-admin.php:604
+#: includes/class-m365-login-admin.php:656
msgid "Every imported user gets this role. The sync manages the roles of imported accounts – manual role changes are overwritten on the next run."
msgstr "Jeder importierte Benutzer erhält diese Rolle. Die Rollen importierter Konten verwaltet der Sync – manuelle Rollenänderungen werden beim nächsten Lauf überschrieben."
-#: includes/class-m365-login-admin.php:607
+#: includes/class-m365-login-admin.php:659
msgid "Additional roles from Microsoft 365 groups"
msgstr "Zusätzliche Rollen aus Microsoft-365-Gruppen"
-#: includes/class-m365-login-admin.php:608
+#: includes/class-m365-login-admin.php:660
msgid "Members of a group (nested memberships count) get the role next to it. If a person leaves the group, the role is removed again on the next sync."
msgstr "Mitglieder einer Gruppe (verschachtelte Mitgliedschaften zählen) erhalten die Rolle daneben. Verlässt eine Person die Gruppe, wird die Rolle beim nächsten Sync wieder entfernt."
-#: includes/class-m365-login-admin.php:609
+#: includes/class-m365-login-admin.php:661
+msgid "Whoever can change a group's members controls the mapped role. For roles with administrative rights use security groups (ideally role-assignable ones) – never public Microsoft 365 groups or Teams, which members can join themselves."
+msgstr "Wer die Mitglieder einer Gruppe ändern kann, bestimmt über die zugeordnete Rolle. Für Rollen mit Administrationsrechten Sicherheitsgruppen verwenden (am besten rollenzuweisbare) – nie öffentliche Microsoft-365-Gruppen oder Teams, denen Mitglieder selbst beitreten können."
+
+#: includes/class-m365-login-admin.php:662
msgid "No group mapping – everybody gets the default role."
msgstr "Keine Gruppenzuordnung – alle erhalten die Standardrolle."
-#: includes/class-m365-login-admin.php:612
+#: includes/class-m365-login-admin.php:665
msgid "How are mapped roles applied?"
msgstr "Wie werden zugeordnete Rollen vergeben?"
-#: includes/class-m365-login-admin.php:615
+#: includes/class-m365-login-admin.php:668
msgid "In addition to the default role (a user can have several roles)"
msgstr "Zusätzlich zur Standardrolle (ein Benutzer kann mehrere Rollen haben)"
-#: includes/class-m365-login-admin.php:619
+#: includes/class-m365-login-admin.php:672
msgid "Instead of the default role – the first matching group in the list wins (use ↑ to reorder)"
msgstr "Anstelle der Standardrolle – die erste passende Gruppe der Liste gewinnt (Reihenfolge mit ↑ ändern)"
-#: includes/class-m365-login-admin.php:626
+#: includes/class-m365-login-admin.php:679
msgid "Also manage the roles of accounts that existed before the sync"
msgstr "Auch die Rollen von Konten verwalten, die schon vor dem Sync existierten"
-#: includes/class-m365-login-admin.php:627
+#: includes/class-m365-login-admin.php:680
msgid "Off: existing accounts are only linked and get their profile fields updated; their roles stay as they are. Administrators that existed before the sync and your own account are never changed."
msgstr "Aus: Bestehende Konten werden nur verknüpft und ihre Profilfelder aktualisiert; ihre Rollen bleiben, wie sie sind. Administratoren, die schon vor dem Sync existierten, und Ihr eigenes Konto werden nie verändert."
-#: includes/class-m365-login-admin.php:633
+#: includes/class-m365-login-admin.php:686
msgid "Profile fields"
msgstr "Profilfelder"
-#: includes/class-m365-login-admin.php:634
+#: includes/class-m365-login-admin.php:687
msgid "Selected Microsoft 365 attributes are copied into the WordPress profile on every sync (Microsoft 365 wins). Name fields go into the standard profile fields, everything else into user meta keys starting with \"m365_\" – usable by themes and other plugins – and is shown on the profile screen."
msgstr "Ausgewählte Microsoft-365-Attribute werden bei jedem Sync ins WordPress-Profil übernommen (Microsoft 365 hat Vorrang). Namen landen in den normalen Profilfeldern, alles andere in Benutzer-Metadaten mit dem Präfix „m365_“ – nutzbar für Themes und andere Plugins – und wird auf der Profilseite angezeigt."
-#: includes/class-m365-login-admin.php:643
+#: includes/class-m365-login-admin.php:696
msgid "Profile photos are stored in wp-content/uploads/m365-login-avatars/ and replace the Gravatar. They are compared on every run: changed photos are downloaded again, photos deleted in Microsoft 365 are deleted in WordPress too. Fields and photos you deselect here are removed from the profiles on the next run (first and last name and display name stay)."
msgstr "Profilbilder werden in wp-content/uploads/m365-login-avatars/ gespeichert und ersetzen den Gravatar. Sie werden bei jedem Lauf abgeglichen: Geänderte Bilder werden neu geladen, in Microsoft 365 gelöschte Bilder auch in WordPress gelöscht. Felder und Bilder, die Sie hier abwählen, werden beim nächsten Lauf aus den Profilen entfernt (Vor-, Nach- und Anzeigename bleiben)."
-#: includes/class-m365-login-admin.php:647
+#: includes/class-m365-login-admin.php:700
msgid "Disabled and deleted Microsoft 365 accounts"
msgstr "Deaktivierte und gelöschte Microsoft-365-Konten"
-#: includes/class-m365-login-admin.php:648
+#: includes/class-m365-login-admin.php:701
msgid "Applies to WordPress accounts linked to a Microsoft account (imported, or signed in with Microsoft at least once). Deactivated accounts cannot sign in at all – not with Microsoft, a password or an application password – and are signed out immediately. When the person is active in Microsoft 365 again, the sync reactivates the account."
msgstr "Gilt für WordPress-Konten, die mit einem Microsoft-Konto verknüpft sind (importiert oder mindestens einmal per Microsoft angemeldet). Deaktivierte Konten können sich gar nicht mehr anmelden – weder mit Microsoft noch mit Passwort oder Anwendungspasswort – und werden sofort abgemeldet. Ist die Person in Microsoft 365 wieder aktiv, reaktiviert der Sync das Konto."
-#: includes/class-m365-login-admin.php:659
+#: includes/class-m365-login-admin.php:712
msgid "Only relevant when the import is limited to groups."
msgstr "Nur relevant, wenn der Import auf Gruppen beschränkt ist."
-#: includes/class-m365-login-admin.php:665
+#: includes/class-m365-login-admin.php:718
msgid "Posts of deleted accounts go to"
msgstr "Beiträge gelöschter Konten übernimmt"
-#: includes/class-m365-login-admin.php:673
+#: includes/class-m365-login-admin.php:726
msgid "— Select a user —"
msgstr "— Benutzer auswählen —"
-#: includes/class-m365-login-admin.php:680
+#: includes/class-m365-login-admin.php:733
msgid "Required for \"Delete\". Without a user, accounts are deactivated instead, so no content is ever lost."
msgstr "Erforderlich für „Löschen“. Ohne Benutzer werden Konten stattdessen deaktiviert, damit nie Inhalte verloren gehen."
-#: includes/class-m365-login-admin.php:683
+#: includes/class-m365-login-admin.php:736
msgid "Safety stop: if a run would deactivate or delete more than 20 % of the linked accounts (at least 5), nothing is deactivated or deleted and the run is reported as stopped. A failed Microsoft Graph request also stops the run before anything is deactivated."
msgstr "Sicherheitsstopp: Würde ein Lauf mehr als 20 % der verknüpften Konten (mindestens 5) deaktivieren oder löschen, wird nichts deaktiviert oder gelöscht und der Lauf als gestoppt gemeldet. Auch eine fehlgeschlagene Microsoft-Graph-Anfrage stoppt den Lauf, bevor etwas deaktiviert wird."
-#: includes/class-m365-login-admin.php:687
+#: includes/class-m365-login-admin.php:740
msgid "Run the sync"
msgstr "Sync ausführen"
-#: includes/class-m365-login-admin.php:688
+#: includes/class-m365-login-admin.php:741
msgid "The run uses the saved settings. Start with a dry run: it reads Microsoft 365 and lists what would change, without changing anything."
msgstr "Der Lauf verwendet die gespeicherten Einstellungen. Beginnen Sie mit einem Testlauf: Er liest Microsoft 365 und listet auf, was sich ändern würde, ohne etwas zu ändern."
-#: includes/class-m365-login-admin.php:690
+#: includes/class-m365-login-admin.php:743
msgid "Dry run"
msgstr "Testlauf"
-#: includes/class-m365-login-admin.php:691
+#: includes/class-m365-login-admin.php:744
msgid "Sync now"
msgstr "Jetzt synchronisieren"
-#: includes/class-m365-login-admin.php:693
+#: includes/class-m365-login-admin.php:746
msgid "Required application permissions (Microsoft Graph, admin consent): User.Read.All, and GroupMember.Read.All when groups are used."
msgstr "Benötigte Anwendungsberechtigungen (Microsoft Graph, Administratorzustimmung): User.Read.All, bei Verwendung von Gruppen zusätzlich GroupMember.Read.All."
-#: includes/class-m365-login-admin.php:707
+#: includes/class-m365-login-admin.php:760
msgid "You are not allowed to access this page."
msgstr "Für diese Seite fehlt die Berechtigung."
-#: includes/class-m365-login-admin.php:726
+#: includes/class-m365-login-admin.php:779
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr "Bestehende Benutzer melden sich mit ihrem Microsoft 365 / Entra ID-Konto an."
-#: includes/class-m365-login-admin.php:731
+#: includes/class-m365-login-admin.php:784
msgid "Connected"
msgstr "Verbunden"
-#: includes/class-m365-login-admin.php:731
+#: includes/class-m365-login-admin.php:784
msgid "Setup incomplete"
msgstr "Einrichtung unvollständig"
-#: includes/class-m365-login-admin.php:753
+#: includes/class-m365-login-admin.php:806
msgid "Microsoft Entra ID app registration"
msgstr "App-Registrierung in Microsoft Entra ID"
-#: includes/class-m365-login-admin.php:754
+#: includes/class-m365-login-admin.php:807
msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr "Tragen Sie hier die Werte aus Ihrer App-Registrierung im Microsoft Entra Admin Center ein."
-#: includes/class-m365-login-admin.php:757
+#: includes/class-m365-login-admin.php:810
msgid "Directory (tenant) ID"
msgstr "Verzeichnis-ID (Mandant/Tenant)"
-#: includes/class-m365-login-admin.php:760
+#: includes/class-m365-login-admin.php:813
msgid "Test tenant"
msgstr "Tenant testen"
-#: includes/class-m365-login-admin.php:762
+#: includes/class-m365-login-admin.php:815
msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
msgstr "Empfohlen: die GUID Ihres Tenants. Dann werden nur Anmeldungen aus diesem Tenant akzeptiert. „organizations“ erlaubt beliebige Geschäfts-, Schul- oder Unikonten."
-#: includes/class-m365-login-admin.php:764
+#: includes/class-m365-login-admin.php:817
msgid "Multi-tenant mode: accounts from any Microsoft tenant can sign in. Their \"email\" attribute is not verified, so the plugin matches on the user principal name (verified domain) only and ignores the e-mail claim unless Microsoft marks it as domain-verified. Use the e-mail domain allow-list on the Security tab, or better, pin your tenant GUID."
msgstr "Multi-Tenant-Modus: Konten aus beliebigen Microsoft-Tenants können sich anmelden. Deren „email“-Attribut ist nicht verifiziert, deshalb ordnet das Plugin nur über den User Principal Name (verifizierte Domain) zu und ignoriert den E-Mail-Claim, sofern Microsoft ihn nicht als domain-verifiziert markiert. Nutze die Domain-Allowlist im Tab „Sicherheit“ oder besser: die Tenant-GUID eintragen."
-#: includes/class-m365-login-admin.php:770
+#: includes/class-m365-login-admin.php:823
msgid "Application (client) ID"
msgstr "Anwendungs-ID (Client)"
-#: includes/class-m365-login-admin.php:775
+#: includes/class-m365-login-admin.php:828
msgid "How should WordPress authenticate to Microsoft?"
msgstr "Wie soll sich WordPress bei Microsoft authentifizieren?"
-#: includes/class-m365-login-admin.php:780 includes/class-m365-login-admin.php:797
+#: includes/class-m365-login-admin.php:833 includes/class-m365-login-admin.php:850
msgid "Client secret"
msgstr "Geheimer Clientschlüssel (Client Secret)"
-#: includes/class-m365-login-admin.php:781
+#: includes/class-m365-login-admin.php:834
msgid "Quick to set up. A password-like value created in Entra ID that expires after 6–24 months and must be renewed."
msgstr "Schnell eingerichtet. Ein passwortähnlicher Wert aus Entra ID, der nach 6–24 Monaten abläuft und erneuert werden muss."
-#: includes/class-m365-login-admin.php:787
+#: includes/class-m365-login-admin.php:840
msgid "Certificate"
msgstr "Zertifikat"
-#: includes/class-m365-login-admin.php:787
+#: includes/class-m365-login-admin.php:840
msgid "Recommended"
msgstr "Empfohlen"
-#: includes/class-m365-login-admin.php:788
+#: includes/class-m365-login-admin.php:841
msgid "The private key never leaves this server; only the public certificate is uploaded to Entra ID. Generated here with one click, valid for 2 years."
msgstr "Der private Schlüssel verlässt diesen Server nie; nur das öffentliche Zertifikat wird in Entra ID hochgeladen. Mit einem Klick hier erzeugt, 2 Jahre gültig."
-#: includes/class-m365-login-admin.php:799
+#: includes/class-m365-login-admin.php:852
msgid "•••••••••••• (stored, leave empty to keep)"
msgstr "•••••••••••• (gespeichert – leer lassen, um zu behalten)"
-#: includes/class-m365-login-admin.php:799
+#: includes/class-m365-login-admin.php:852
msgid "Paste the secret value"
msgstr "Wert des Secrets einfügen"
-#: includes/class-m365-login-admin.php:800
+#: includes/class-m365-login-admin.php:853
msgid "Show secret"
msgstr "Secret anzeigen"
-#: includes/class-m365-login-admin.php:805
+#: includes/class-m365-login-admin.php:858
msgid "Remove the stored secret"
msgstr "Gespeichertes Secret entfernen"
-#: includes/class-m365-login-admin.php:808
+#: includes/class-m365-login-admin.php:861
msgid "Stored encrypted (AES-256-GCM, key derived from your WordPress salts) and never displayed again. Client secrets expire – note the expiry date in Entra ID."
msgstr "Wird verschlüsselt gespeichert (AES-256-GCM, Schlüssel aus den WordPress-Salts abgeleitet) und nie wieder angezeigt. Client Secrets laufen ab – Ablaufdatum in Entra ID notieren."
-#: includes/class-m365-login-admin.php:812
+#: includes/class-m365-login-admin.php:863
+msgid "AUTH_KEY and SECURE_AUTH_KEY are not defined in wp-config.php, so WordPress keeps its salts in the database – right next to the encrypted secret. Add the salts to wp-config.php to make the encryption effective."
+msgstr "AUTH_KEY und SECURE_AUTH_KEY sind nicht in der wp-config.php definiert, daher speichert WordPress seine Salts in der Datenbank – direkt neben dem verschlüsselten Secret. Tragen Sie die Salts in die wp-config.php ein, damit die Verschlüsselung wirkt."
+
+#: includes/class-m365-login-admin.php:868
msgid "Step-by-step: create a client secret in Entra ID"
msgstr "Schritt für Schritt: Client Secret in Entra ID erstellen"
-#: includes/class-m365-login-admin.php:815
+#: includes/class-m365-login-admin.php:871
msgid "Open entra.microsoft.com and sign in with an account that has the \"Application Administrator\" or \"Global Administrator\" role."
msgstr "entra.microsoft.com öffnen und mit einem Konto anmelden, das die Rolle „Anwendungsadministrator“ oder „Globaler Administrator“ hat."
-#: includes/class-m365-login-admin.php:816
+#: includes/class-m365-login-admin.php:872
msgid "Go to Identity → Applications → App registrations and open your app (or create it first, see the general guide in the sidebar)."
msgstr "Zu Identität → Anwendungen → App-Registrierungen wechseln und die App öffnen (oder zuerst anlegen, siehe allgemeine Anleitung in der Seitenleiste)."
-#: includes/class-m365-login-admin.php:817
+#: includes/class-m365-login-admin.php:873
msgid "In the left menu choose Certificates & secrets, then the tab Client secrets, and click New client secret."
msgstr "Im linken Menü „Zertifikate & Geheimnisse“ wählen, dann den Reiter „Geheime Clientschlüssel“ und auf „Neuer geheimer Clientschlüssel“ klicken."
-#: includes/class-m365-login-admin.php:818
+#: includes/class-m365-login-admin.php:874
msgid "Enter a description such as \"WordPress login\" and pick an expiry. Microsoft allows at most 24 months; put a reminder in your calendar two weeks before."
msgstr "Eine Beschreibung wie „WordPress Login“ eingeben und eine Gültigkeit wählen. Microsoft erlaubt maximal 24 Monate; zwei Wochen vor Ablauf eine Kalender-Erinnerung setzen."
-#: includes/class-m365-login-admin.php:819
+#: includes/class-m365-login-admin.php:875
msgid "Click Add. Copy the Value column immediately – it is shown only once. The Secret ID column is NOT what you need."
msgstr "Auf „Hinzufügen“ klicken. Die Spalte „Wert“ sofort kopieren – sie wird nur einmal angezeigt. Die Spalte „Geheimnis-ID“ ist NICHT der gesuchte Wert."
-#: includes/class-m365-login-admin.php:820
+#: includes/class-m365-login-admin.php:876
msgid "Paste the value into the Client secret field above and save this page."
msgstr "Den Wert oben in das Feld „Geheimer Clientschlüssel“ einfügen und diese Seite speichern."
-#: includes/class-m365-login-admin.php:822
+#: includes/class-m365-login-admin.php:878
msgid "When the secret expires, sign-ins fail with \"Could not complete the sign-in with Microsoft\". Create a new secret, paste it here, save, then delete the old one in Entra ID."
msgstr "Läuft das Secret ab, scheitern Anmeldungen mit „Die Anmeldung über Microsoft konnte nicht abgeschlossen werden“. Dann ein neues Secret erstellen, hier einfügen, speichern und das alte in Entra ID löschen."
-#: includes/class-m365-login-admin.php:835
+#: includes/class-m365-login-admin.php:891
msgid "Expired"
msgstr "Abgelaufen"
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:839
+#: includes/class-m365-login-admin.php:895
msgid "Expires in %d days"
msgstr "Läuft in %d Tagen ab"
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:842
+#: includes/class-m365-login-admin.php:898
msgid "Valid"
msgstr "Gültig"
-#: includes/class-m365-login-admin.php:847
+#: includes/class-m365-login-admin.php:903
msgid "Thumbprint (SHA-1)"
msgstr "Fingerabdruck (SHA-1)"
-#: includes/class-m365-login-admin.php:849
+#: includes/class-m365-login-admin.php:905
msgid "Subject"
msgstr "Antragsteller"
-#: includes/class-m365-login-admin.php:851
+#: includes/class-m365-login-admin.php:907
msgid "Key size"
msgstr "Schlüssellänge"
-#: includes/class-m365-login-admin.php:853
+#: includes/class-m365-login-admin.php:909
msgid "Valid until"
msgstr "Gültig bis"
-#: includes/class-m365-login-admin.php:857
+#: includes/class-m365-login-admin.php:913
msgid "Download certificate (.cer)"
msgstr "Zertifikat herunterladen (.cer)"
-#: includes/class-m365-login-admin.php:858
+#: includes/class-m365-login-admin.php:914
msgid "Generate new certificate"
msgstr "Neues Zertifikat erzeugen"
-#: includes/class-m365-login-admin.php:861
+#: includes/class-m365-login-admin.php:917
msgid "Remove certificate when saving"
msgstr "Zertifikat beim Speichern entfernen"
-#: includes/class-m365-login-admin.php:865
+#: includes/class-m365-login-admin.php:921
msgid "No certificate stored yet."
msgstr "Noch kein Zertifikat gespeichert."
-#: includes/class-m365-login-admin.php:867
+#: includes/class-m365-login-admin.php:923
msgid "Generate certificate"
msgstr "Zertifikat erzeugen"
-#: includes/class-m365-login-admin.php:868
+#: includes/class-m365-login-admin.php:924
msgid "3072-bit RSA, self-signed, valid for 2 years. The private key is stored encrypted and never shown or downloadable."
msgstr "3072 Bit RSA, selbstsigniert, 2 Jahre gültig. Der private Schlüssel wird verschlüsselt gespeichert und nie angezeigt oder zum Download angeboten."
-#: includes/class-m365-login-admin.php:872
+#: includes/class-m365-login-admin.php:928
msgid "Use your own certificate instead (paste PEM)"
msgstr "Stattdessen eigenes Zertifikat verwenden (PEM einfügen)"
-#: includes/class-m365-login-admin.php:875
+#: includes/class-m365-login-admin.php:931
msgid "Private key (PEM, unencrypted)"
msgstr "Privater Schlüssel (PEM, unverschlüsselt)"
-#: includes/class-m365-login-admin.php:879
+#: includes/class-m365-login-admin.php:935
msgid "Certificate (PEM)"
msgstr "Zertifikat (PEM)"
-#: includes/class-m365-login-admin.php:881
+#: includes/class-m365-login-admin.php:937
msgid "RSA, at least 2048 bits. The pair is validated and the key is encrypted when you save. Both fields stay empty afterwards."
msgstr "RSA, mindestens 2048 Bit. Beim Speichern wird das Paar geprüft und der Schlüssel verschlüsselt. Beide Felder bleiben danach leer."
-#: includes/class-m365-login-admin.php:887
+#: includes/class-m365-login-admin.php:943
msgid "Step-by-step: register the certificate in Entra ID"
msgstr "Schritt für Schritt: Zertifikat in Entra ID hinterlegen"
-#: includes/class-m365-login-admin.php:890
+#: includes/class-m365-login-admin.php:946
msgid "Click Generate certificate above (or paste your own). Then click Download certificate (.cer) – the file contains only the public part."
msgstr "Oben auf „Zertifikat erzeugen“ klicken (oder ein eigenes einfügen). Danach „Zertifikat herunterladen (.cer)“ – die Datei enthält nur den öffentlichen Teil."
-#: includes/class-m365-login-admin.php:891
+#: includes/class-m365-login-admin.php:947
msgid "Open entra.microsoft.com → Identity → Applications → App registrations and open your app."
msgstr "entra.microsoft.com → Identität → Anwendungen → App-Registrierungen öffnen und die App auswählen."
-#: includes/class-m365-login-admin.php:892
+#: includes/class-m365-login-admin.php:948
msgid "Choose Certificates & secrets in the left menu, then the tab Certificates, and click Upload certificate."
msgstr "Im linken Menü „Zertifikate & Geheimnisse“ wählen, dann den Reiter „Zertifikate“ und auf „Zertifikat hochladen“ klicken."
-#: includes/class-m365-login-admin.php:893
+#: includes/class-m365-login-admin.php:949
msgid "Select the downloaded .cer file, add a description such as \"WordPress login\" and click Add."
msgstr "Die heruntergeladene .cer-Datei auswählen, eine Beschreibung wie „WordPress Login“ eingeben und auf „Hinzufügen“ klicken."
-#: includes/class-m365-login-admin.php:894
+#: includes/class-m365-login-admin.php:950
msgid "Compare the thumbprint Entra ID shows with the thumbprint above – they must match exactly."
msgstr "Den in Entra ID angezeigten Fingerabdruck mit dem Fingerabdruck oben vergleichen – beide müssen exakt übereinstimmen."
-#: includes/class-m365-login-admin.php:895
+#: includes/class-m365-login-admin.php:951
msgid "Make sure Certificate is selected above and save this page. If a client secret was stored before, you may delete it in Entra ID now."
msgstr "Sicherstellen, dass oben „Zertifikat“ ausgewählt ist, und diese Seite speichern. War vorher ein Client Secret gespeichert, kann es jetzt in Entra ID gelöscht werden."
-#: includes/class-m365-login-admin.php:897
+#: includes/class-m365-login-admin.php:953
msgid "How it works: for every token request WordPress signs a short-lived JWT (client assertion) with the private key; Microsoft verifies it with the uploaded certificate. Nothing secret is ever transmitted."
msgstr "So funktioniert es: Für jede Token-Anfrage signiert WordPress ein kurzlebiges JWT (Client Assertion) mit dem privaten Schlüssel; Microsoft prüft es mit dem hochgeladenen Zertifikat. Es wird nie ein Geheimnis übertragen."
-#: includes/class-m365-login-admin.php:898
+#: includes/class-m365-login-admin.php:954
msgid "Before the certificate expires: generate a new one here, upload it to Entra ID (both may be registered at the same time), save, then remove the old one from Entra ID. Sign-ins keep working during the switch."
msgstr "Vor Ablauf des Zertifikats: hier ein neues erzeugen, in Entra ID hochladen (beide dürfen gleichzeitig hinterlegt sein), speichern und danach das alte in Entra ID entfernen. Anmeldungen funktionieren während des Wechsels weiter."
-#: includes/class-m365-login-admin.php:904
+#: includes/class-m365-login-admin.php:960
msgid "Account prompt"
msgstr "Kontoauswahl"
-#: includes/class-m365-login-admin.php:906
+#: includes/class-m365-login-admin.php:962
msgid "Always let the user pick an account (recommended)"
msgstr "Benutzer wählt immer ein Konto aus (empfohlen)"
-#: includes/class-m365-login-admin.php:907
+#: includes/class-m365-login-admin.php:963
msgid "Use the current Microsoft session if available"
msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden"
-#: includes/class-m365-login-admin.php:908
+#: includes/class-m365-login-admin.php:964
msgid "Always require re-entering credentials"
msgstr "Immer erneute Eingabe der Anmeldedaten verlangen"
-#: includes/class-m365-login-admin.php:917
+#: includes/class-m365-login-admin.php:973
msgid "Appearance"
msgstr "Darstellung"
-#: includes/class-m365-login-admin.php:920
+#: includes/class-m365-login-admin.php:976
msgid "Live preview"
msgstr "Live-Vorschau"
-#: includes/class-m365-login-admin.php:934
+#: includes/class-m365-login-admin.php:990
msgid "Button text"
msgstr "Button-Text"
-#: includes/class-m365-login-admin.php:938
+#: includes/class-m365-login-admin.php:994
msgid "Divider text"
msgstr "Trennlinien-Text"
-#: includes/class-m365-login-admin.php:940
+#: includes/class-m365-login-admin.php:996
msgid "Leave empty to hide the divider line."
msgstr "Leer lassen, um die Trennlinie auszublenden."
-#: includes/class-m365-login-admin.php:945
+#: includes/class-m365-login-admin.php:1001
msgid "Icon"
msgstr "Icon"
-#: includes/class-m365-login-admin.php:948
+#: includes/class-m365-login-admin.php:1004
msgid "Show an icon on the button"
msgstr "Icon auf dem Button anzeigen"
-#: includes/class-m365-login-admin.php:959
+#: includes/class-m365-login-admin.php:1015
msgid "Default: Microsoft logo"
msgstr "Standard: Microsoft-Logo"
-#: includes/class-m365-login-admin.php:961
+#: includes/class-m365-login-admin.php:1017
msgid "Choose from media library"
msgstr "Aus Mediathek wählen"
-#: includes/class-m365-login-admin.php:962
+#: includes/class-m365-login-admin.php:1018
msgid "Use Microsoft logo"
msgstr "Microsoft-Logo verwenden"
-#: includes/class-m365-login-admin.php:964
+#: includes/class-m365-login-admin.php:1020
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
msgstr "PNG, SVG, JPG oder WebP. Quadratische Bilder (z. B. 64×64 px) eignen sich am besten."
-#: includes/class-m365-login-admin.php:972
+#: includes/class-m365-login-admin.php:1028
msgid "Background"
msgstr "Hintergrund"
-#: includes/class-m365-login-admin.php:973
+#: includes/class-m365-login-admin.php:1029
msgid "Background (hover)"
msgstr "Hintergrund (Hover)"
-#: includes/class-m365-login-admin.php:974
+#: includes/class-m365-login-admin.php:1030
msgid "Text colour"
msgstr "Textfarbe"
-#: includes/class-m365-login-admin.php:975
+#: includes/class-m365-login-admin.php:1031
msgid "Border"
msgstr "Rahmen"
-#: includes/class-m365-login-admin.php:988
+#: includes/class-m365-login-admin.php:1044
msgid "Corner radius"
msgstr "Eckenradius"
-#: includes/class-m365-login-admin.php:992
+#: includes/class-m365-login-admin.php:1048
msgid "Position on the login page"
msgstr "Position auf der Login-Seite"
-#: includes/class-m365-login-admin.php:994
+#: includes/class-m365-login-admin.php:1050
msgid "Below the login form"
msgstr "Unter dem Login-Formular"
-#: includes/class-m365-login-admin.php:995
+#: includes/class-m365-login-admin.php:1051
msgid "Above the login form"
msgstr "Über dem Login-Formular"
-#: includes/class-m365-login-admin.php:1001
+#: includes/class-m365-login-admin.php:1057
msgid "Quick presets"
msgstr "Schnellauswahl"
-#: includes/class-m365-login-admin.php:1002
+#: includes/class-m365-login-admin.php:1058
msgid "Microsoft dark"
msgstr "Microsoft dunkel"
-#: includes/class-m365-login-admin.php:1003
+#: includes/class-m365-login-admin.php:1059
msgid "Microsoft light"
msgstr "Microsoft hell"
-#: includes/class-m365-login-admin.php:1004
+#: includes/class-m365-login-admin.php:1060
msgid "Azure blue"
msgstr "Azure-Blau"
-#: includes/class-m365-login-admin.php:1005
+#: includes/class-m365-login-admin.php:1061
msgid "WordPress blue"
msgstr "WordPress-Blau"
-#: includes/class-m365-login-admin.php:1009
+#: includes/class-m365-login-admin.php:1065
msgid "Custom login page"
msgstr "Eigene Login-Seite"
-#: includes/class-m365-login-admin.php:1010
+#: includes/class-m365-login-admin.php:1066
msgid "Using your own login page instead of wp-login.php? Tell the plugin where it is so error messages, the fallback link and the post-logout redirect point there."
msgstr "Eigene Login-Seite statt wp-login.php? Hier eintragen, damit Fehlermeldungen, der Fallback-Link und die Weiterleitung nach dem Abmelden dorthin zeigen."
-#: includes/class-m365-login-admin.php:1013
+#: includes/class-m365-login-admin.php:1069
msgid "URL of your login page"
msgstr "URL der Login-Seite"
-#: includes/class-m365-login-admin.php:1015
+#: includes/class-m365-login-admin.php:1071
msgid "Must be on this site. Leave empty to use wp-login.php."
msgstr "Muss auf dieser Website liegen. Leer lassen, um wp-login.php zu verwenden."
-#: includes/class-m365-login-admin.php:1021
+#: includes/class-m365-login-admin.php:1077
msgid "Add the button to every wp_login_form() form automatically"
msgstr "Button automatisch in jedes wp_login_form()-Formular einfügen"
-#: includes/class-m365-login-admin.php:1022
+#: includes/class-m365-login-admin.php:1078
msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below."
msgstr "Deckt Themes und Plugins ab, die die WordPress-Login-Formularfunktion verwenden. Page-Builder-Widgets benötigen den Shortcode oder die Template-Funktion unten."
-#: includes/class-m365-login-admin.php:1027
+#: includes/class-m365-login-admin.php:1083
msgid "Manual placement"
msgstr "Manuelle Platzierung"
-#: includes/class-m365-login-admin.php:1028
+#: includes/class-m365-login-admin.php:1084
msgid "Shortcode (block editor, page builders):"
msgstr "Shortcode (Block-Editor, Page Builder):"
-#: includes/class-m365-login-admin.php:1030
+#: includes/class-m365-login-admin.php:1086
msgid "Template function (theme files):"
msgstr "Template-Funktion (Theme-Dateien):"
-#: includes/class-m365-login-admin.php:1032
+#: includes/class-m365-login-admin.php:1088
msgid "Both show the error messages of the last attempt; use m365_login_messages() to place them separately."
msgstr "Beide zeigen die Fehlermeldungen des letzten Versuchs; mit m365_login_messages() lassen sie sich separat platzieren."
-#: includes/class-m365-login-admin.php:1040
+#: includes/class-m365-login-admin.php:1096
msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung"
-#: includes/class-m365-login-admin.php:1041
+#: includes/class-m365-login-admin.php:1097
msgid "Sign-in never creates users. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists – created by hand or imported by the user sync."
msgstr "Die Anmeldung legt nie Benutzer an. Eine Microsoft-Anmeldung klappt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert – von Hand angelegt oder vom Benutzer-Sync importiert."
-#: includes/class-m365-login-admin.php:1046
+#: includes/class-m365-login-admin.php:1102
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
-#: includes/class-m365-login-admin.php:1047
+#: includes/class-m365-login-admin.php:1103
msgid "On first sign-in the immutable Microsoft object ID is stored with the user. Later sign-ins with the same e-mail but a different Microsoft identity are rejected. Strongly recommended."
msgstr "Bei der ersten Anmeldung wird die unveränderliche Microsoft-Objekt-ID am Benutzer gespeichert. Spätere Anmeldungen mit gleicher E-Mail, aber anderer Microsoft-Identität werden abgelehnt. Dringend empfohlen."
-#: includes/class-m365-login-admin.php:1054
+#: includes/class-m365-login-admin.php:1110
msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen"
-#: includes/class-m365-login-admin.php:1055
+#: includes/class-m365-login-admin.php:1111
msgid "If the token contains no \"email\" claim, use the UPN (e.g. jane@contoso.com) when it is a valid e-mail address. Usually required for work accounts."
msgstr "Enthält das Token keinen „email“-Claim, wird der UPN (z. B. jane@contoso.com) verwendet, sofern er eine gültige E-Mail-Adresse ist. Für Geschäftskonten meist erforderlich."
-#: includes/class-m365-login-admin.php:1062
+#: includes/class-m365-login-admin.php:1118
msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
-#: includes/class-m365-login-admin.php:1063
+#: includes/class-m365-login-admin.php:1119
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
-#: includes/class-m365-login-admin.php:1068
+#: includes/class-m365-login-admin.php:1124
msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)"
-#: includes/class-m365-login-admin.php:1070
+#: includes/class-m365-login-admin.php:1126
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr "Eine oder mehrere Domains, getrennt durch Kommas oder Zeilenumbrüche. Leer lassen, um alle Domains des Tenants zuzulassen."
-#: includes/class-m365-login-admin.php:1075
+#: includes/class-m365-login-admin.php:1131
msgid "Allowed Entra groups (optional)"
msgstr "Erlaubte Entra-Gruppen (optional)"
-#: includes/class-m365-login-admin.php:1076
+#: includes/class-m365-login-admin.php:1132
msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
msgstr "Nur Mitglieder mindestens einer dieser Gruppen dürfen sich anmelden. Leer lassen, um alle zugeordneten Benutzer zuzulassen. Verschachtelte Mitgliedschaften zählen."
-#: includes/class-m365-login-admin.php:1078
+#: includes/class-m365-login-admin.php:1134
msgid "No groups selected – every matched user may sign in."
msgstr "Keine Gruppen ausgewählt – jeder zugeordnete Benutzer darf sich anmelden."
-#: includes/class-m365-login-admin.php:1080
+#: includes/class-m365-login-admin.php:1136
msgid "Membership is read from the \"groups\" claim of the ID token when present; otherwise the plugin asks Microsoft Graph (application permission \"User.Read.All\" or \"Directory.Read.All\"). If neither works, the sign-in is refused."
msgstr "Die Mitgliedschaft wird aus dem „groups“-Claim des ID-Tokens gelesen, falls vorhanden; andernfalls fragt das Plugin Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“). Funktioniert beides nicht, wird die Anmeldung abgelehnt."
-#: includes/class-m365-login-admin.php:1085
+#: includes/class-m365-login-admin.php:1141
msgid "Excluded Entra groups (optional)"
msgstr "Ausgeschlossene Entra-Gruppen (optional)"
-#: includes/class-m365-login-admin.php:1086
+#: includes/class-m365-login-admin.php:1142
msgid "Members of these groups can never sign in with Microsoft – even if they are in an allowed group. Nested memberships count."
msgstr "Mitglieder dieser Gruppen können sich nie per Microsoft anmelden – auch nicht, wenn sie in einer erlaubten Gruppe sind. Verschachtelte Mitgliedschaften zählen."
-#: includes/class-m365-login-admin.php:1088
+#: includes/class-m365-login-admin.php:1145
+msgid "Group rules need a pinned tenant ID (GUID) on the Connection tab. In multi-tenant mode the group check cannot ask Microsoft Graph, so every sign-in is refused while groups are selected here or above."
+msgstr "Gruppenregeln brauchen eine feste Tenant-ID (GUID) im Tab „Verbindung“. Im Multi-Tenant-Modus kann die Gruppenprüfung Microsoft Graph nicht fragen, deshalb wird jede Anmeldung abgelehnt, solange hier oder oben Gruppen ausgewählt sind."
+
+#: includes/class-m365-login-admin.php:1147
msgid "No groups excluded."
msgstr "Keine Gruppen ausgeschlossen."
-#: includes/class-m365-login-admin.php:1090
+#: includes/class-m365-login-admin.php:1149
msgid "The plugin asks Microsoft Graph on every sign-in (application permission \"User.Read.All\" or \"Directory.Read.All\"), because a \"groups\" claim may be filtered and cannot prove that someone is not a member. If the check fails, the sign-in is refused. Password sign-in is not affected – combine with button-only mode if needed."
msgstr "Das Plugin fragt bei jeder Anmeldung Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“), weil ein „groups“-Claim gefiltert sein kann und nicht beweist, dass jemand kein Mitglied ist. Schlägt die Prüfung fehl, wird die Anmeldung abgelehnt. Die Passwort-Anmeldung ist nicht betroffen – bei Bedarf mit dem Nur-Button-Modus kombinieren."
-#: includes/class-m365-login-admin.php:1095
+#: includes/class-m365-login-admin.php:1154
msgid "Button-only mode"
msgstr "Nur-Button-Modus"
-#: includes/class-m365-login-admin.php:1096
-msgid "Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every interactive password sign-in on the site, including custom login forms. Application passwords, REST, XML-RPC and WP-CLI are not affected."
-msgstr "Blendet die Benutzername/Passwort-Felder aus (auf wp-login.php und in wp_login_form()-Formularen) und lehnt jede interaktive Passwort-Anmeldung auf der Website ab, auch in eigenen Login-Formularen. Anwendungspasswörter, REST, XML-RPC und WP-CLI sind nicht betroffen."
+#: includes/class-m365-login-admin.php:1155
+msgid "Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every sign-in with a normal password on the site – custom login forms, XML-RPC and login endpoints of other plugins included. Application passwords (REST, XML-RPC) and WP-CLI keep working; API requests never receive a login cookie."
+msgstr "Blendet die Felder für Benutzername/Passwort aus (auf wp-login.php und in wp_login_form()-Formularen) und lehnt jede Anmeldung mit einem normalen Passwort auf der Website ab – auch in eigenen Login-Formularen, über XML-RPC und über Login-Endpunkte anderer Plugins. Anwendungspasswörter (REST, XML-RPC) und WP-CLI funktionieren weiter; API-Anfragen erhalten nie ein Login-Cookie."
-#: includes/class-m365-login-admin.php:1101
+#: includes/class-m365-login-admin.php:1160
msgid "Show only the Microsoft button on the login page"
msgstr "Auf der Login-Seite nur den Microsoft-Button anzeigen"
-#: includes/class-m365-login-admin.php:1102
+#: includes/class-m365-login-admin.php:1161
msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
msgstr "Wird aktiv, sobald die Verbindung eingerichtet ist. Vor dem Aktivieren sicherstellen, dass das eigene Konto sich per Microsoft anmelden kann."
-#: includes/class-m365-login-admin.php:1107
+#: includes/class-m365-login-admin.php:1166
msgid "Fallback link (keep it secret)"
msgstr "Fallback-Link (geheim halten)"
-#: includes/class-m365-login-admin.php:1108
+#: includes/class-m365-login-admin.php:1167
msgid "Opening this link shows the password form again in that browser for 30 minutes and allows password sign-in there. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks."
msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular und kann sich dort mit Passwort anmelden. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
-#: includes/class-m365-login-admin.php:1116
+#: includes/class-m365-login-admin.php:1175
msgid "Generate a new key when saving"
msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
-#: includes/class-m365-login-admin.php:1119
+#: includes/class-m365-login-admin.php:1178
msgid "A key is generated automatically the first time you save these settings."
msgstr "Beim ersten Speichern dieser Einstellungen wird automatisch ein Schlüssel erzeugt."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:1125
+#: includes/class-m365-login-admin.php:1184
msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
msgstr "Notschalter: %s in die wp-config.php eintragen, um den Nur-Button-Modus vollständig abzuschalten."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:1134
+#: includes/class-m365-login-admin.php:1193
msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung"
-#: includes/class-m365-login-admin.php:1136
+#: includes/class-m365-login-admin.php:1195
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) – Tokens laufen nie durch den Browser."
-#: includes/class-m365-login-admin.php:1137
+#: includes/class-m365-login-admin.php:1196
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
-#: includes/class-m365-login-admin.php:1138
+#: includes/class-m365-login-admin.php:1197
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr "Signatur des ID-Tokens wird gegen Microsofts veröffentlichte Signaturschlüssel geprüft; Issuer, Audience, Tenant, Ablauf und Nonce werden kontrolliert."
-#: includes/class-m365-login-admin.php:1139
+#: includes/class-m365-login-admin.php:1198
msgid "Client secret encrypted at rest; sign-in never creates accounts or changes passwords."
msgstr "Client Secret verschlüsselt gespeichert; die Anmeldung legt nie Konten an und ändert keine Passwörter."
-#: includes/class-m365-login-admin.php:1147
+#: includes/class-m365-login-admin.php:1206
msgid "Save changes"
msgstr "Änderungen speichern"
-#: includes/class-m365-login-admin.php:1153
+#: includes/class-m365-login-admin.php:1212
msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)"
-#: includes/class-m365-login-admin.php:1154
+#: includes/class-m365-login-admin.php:1213
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
-#: includes/class-m365-login-admin.php:1160
+#: includes/class-m365-login-admin.php:1219
msgid "Plain permalinks are active, so the callback uses a query string. If you enable pretty permalinks later, the redirect URI changes and must be updated in Entra ID."
msgstr "Einfache Permalinks sind aktiv, daher verwendet der Callback einen Query-String. Werden später sprechende Permalinks aktiviert, ändert sich die Umleitungs-URI und muss in Entra ID angepasst werden."
-#: includes/class-m365-login-admin.php:1163
+#: includes/class-m365-login-admin.php:1222
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr "Diese Website nutzt kein HTTPS. Microsoft akzeptiert http://-Umleitungs-URIs nur für localhost; produktive Websites benötigen HTTPS."
-#: includes/class-m365-login-admin.php:1168
+#: includes/class-m365-login-admin.php:1227
msgid "Setup guide: app registration"
msgstr "Anleitung: App-Registrierung"
-#: includes/class-m365-login-admin.php:1170
+#: includes/class-m365-login-admin.php:1229
msgid "Open entra.microsoft.com → Identity → Applications → App registrations → New registration."
msgstr "entra.microsoft.com → Identität → Anwendungen → App-Registrierungen → Neue Registrierung öffnen."
-#: includes/class-m365-login-admin.php:1171
+#: includes/class-m365-login-admin.php:1230
msgid "Name: e.g. \"WordPress login\". Supported account types: \"Accounts in this organizational directory only\" (single tenant)."
msgstr "Name: z. B. „WordPress Login“. Unterstützte Kontotypen: „Nur Konten in diesem Organisationsverzeichnis“ (Single Tenant)."
-#: includes/class-m365-login-admin.php:1172
+#: includes/class-m365-login-admin.php:1231
msgid "Redirect URI: choose the platform Web and paste the URI shown above. Then click Register."
msgstr "Umleitungs-URI: Plattform „Web“ wählen und die oben angezeigte URI einfügen. Dann auf „Registrieren“ klicken."
-#: includes/class-m365-login-admin.php:1173
+#: includes/class-m365-login-admin.php:1232
msgid "On the Overview page copy the Application (client) ID and the Directory (tenant) ID into the Connection tab."
msgstr "Auf der Übersichtsseite die Anwendungs-ID (Client) und die Verzeichnis-ID (Mandant) in den Tab „Verbindung“ kopieren."
-#: includes/class-m365-login-admin.php:1174
+#: includes/class-m365-login-admin.php:1233
msgid "Authentication: leave \"ID tokens\" unchecked (the plugin uses the authorization code flow) and \"Allow public client flows\" on No."
msgstr "Authentifizierung: „ID-Token“ nicht anhaken (das Plugin nutzt den Authorization Code Flow) und „Öffentliche Clientflows zulassen“ auf „Nein“ lassen."
-#: includes/class-m365-login-admin.php:1175
+#: includes/class-m365-login-admin.php:1234
msgid "Token configuration → Add optional claim → ID → tick \"email\" → Add. Confirm the API permission prompt."
msgstr "Tokenkonfiguration → Optionalen Anspruch hinzufügen → ID → „email“ anhaken → Hinzufügen. Die Rückfrage zur API-Berechtigung bestätigen."
-#: includes/class-m365-login-admin.php:1176
+#: includes/class-m365-login-admin.php:1235
msgid "Pick the authentication method on the Connection tab and follow its step-by-step guide (client secret or certificate)."
msgstr "Im Tab „Verbindung“ die Authentifizierungsmethode wählen und der zugehörigen Schritt-für-Schritt-Anleitung folgen (Client Secret oder Zertifikat)."
-#: includes/class-m365-login-admin.php:1177
+#: includes/class-m365-login-admin.php:1236
msgid "Optional: restrict who may use the app under Enterprise applications → your app → Properties → \"Assignment required\" = Yes, then assign users/groups."
msgstr "Optional: Unter Unternehmensanwendungen → Ihre App → Eigenschaften → „Zuweisung erforderlich“ = Ja einschränken, wer die App nutzen darf, und dann Benutzer/Gruppen zuweisen."
-#: includes/class-m365-login-admin.php:1179
+#: includes/class-m365-login-admin.php:1238
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) – standardmäßig vorhanden."
-#: includes/class-m365-login-admin.php:1180
+#: includes/class-m365-login-admin.php:1239
msgid "Optional, for group restrictions and the user sync: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
msgstr "Optional für Gruppen-Beschränkungen und den Benutzer-Sync: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
-#: includes/class-m365-login-admin.php:1184
+#: includes/class-m365-login-admin.php:1243
msgid "Shortcode"
msgstr "Shortcode"
-#: includes/class-m365-login-admin.php:1185
+#: includes/class-m365-login-admin.php:1244
msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:"
-#: includes/class-m365-login-admin.php:1187
+#: includes/class-m365-login-admin.php:1246
msgid "More options on the Button tab under \"Custom login page\"."
msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“."
-#: includes/class-m365-login-auth.php:173
+#: includes/class-m365-login-auth.php:182
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr "Die Anmeldung mit Passwort ist auf dieser Website deaktiviert. Bitte den Microsoft-Button verwenden."
-#: includes/class-m365-login-auth.php:906
+#: includes/class-m365-login-auth.php:998
+msgid "Microsoft sign-in is temporarily unavailable. Please contact an administrator."
+msgstr "Die Microsoft-Anmeldung ist vorübergehend nicht verfügbar. Bitte wenden Sie sich an einen Administrator."
+
+#: includes/class-m365-login-auth.php:1005
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr "Die Passwort-Anmeldung ist für diesen Browser vorübergehend aktiviert (30 Minuten)."
-#: includes/class-m365-login-auth.php:927 includes/class-m365-login-graph.php:63
+#: includes/class-m365-login-auth.php:1026 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
-#: includes/class-m365-login-auth.php:928
+#: includes/class-m365-login-auth.php:1027
msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:929
+#: includes/class-m365-login-auth.php:1028
msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
-#: includes/class-m365-login-auth.php:930
+#: includes/class-m365-login-auth.php:1029
msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:931
+#: includes/class-m365-login-auth.php:1030
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:932
+#: includes/class-m365-login-auth.php:1031
msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
-#: includes/class-m365-login-auth.php:933
+#: includes/class-m365-login-auth.php:1032
msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
-#: includes/class-m365-login-auth.php:934
+#: includes/class-m365-login-auth.php:1033
msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
-#: includes/class-m365-login-auth.php:935
+#: includes/class-m365-login-auth.php:1034
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
-#: includes/class-m365-login-auth.php:936
+#: includes/class-m365-login-auth.php:1035
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:937
+#: includes/class-m365-login-auth.php:1036
msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
-#: includes/class-m365-login-auth.php:938
+#: includes/class-m365-login-auth.php:1037
msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
msgstr "Das Microsoft-Konto ist in keiner Gruppe, die sich hier anmelden darf."
-#: includes/class-m365-login-auth.php:939
+#: includes/class-m365-login-auth.php:1038
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr "Die Gruppenmitgliedschaft konnte nicht geprüft werden. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:940
+#: includes/class-m365-login-auth.php:1039
msgid "Your Microsoft account is a member of a group that is not allowed to sign in here."
msgstr "Ihr Microsoft-Konto ist Mitglied einer Gruppe, die sich hier nicht anmelden darf."
-#: includes/class-m365-login-auth.php:941
+#: includes/class-m365-login-auth.php:1040
msgid "The fallback key is not valid."
msgstr "Der Fallback-Schlüssel ist ungültig."
-#: includes/class-m365-login-auth.php:942
+#: includes/class-m365-login-auth.php:1041
msgid "Too many attempts. Please wait 15 minutes."
msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
-#: includes/class-m365-login-auth.php:943
+#: includes/class-m365-login-auth.php:1042
msgid "Too many sign-in attempts from your connection. Please wait a few minutes and try again."
msgstr "Zu viele Anmeldeversuche von dieser Verbindung. Bitte ein paar Minuten warten und erneut versuchen."
-#: includes/class-m365-login-auth.php:944 includes/class-m365-login-sync.php:1520
+#: includes/class-m365-login-auth.php:1043 includes/class-m365-login-sync.php:1684
msgid "This account has been deactivated."
msgstr "Dieses Konto wurde deaktiviert."
+#: includes/class-m365-login-auth.php:1044
+msgid "For security reasons this administrator account can only be linked to a Microsoft account whose user principal name equals the WordPress e-mail address. Please contact an administrator."
+msgstr "Aus Sicherheitsgründen kann dieses Administrator-Konto nur mit einem Microsoft-Konto verknüpft werden, dessen Benutzerprinzipalname der WordPress-E-Mail-Adresse entspricht. Bitte wenden Sie sich an einen Administrator."
+
+#: includes/class-m365-login-auth.php:1045
+msgid "Guest and external accounts cannot sign in here."
+msgstr "Gast- und externe Konten können sich hier nicht anmelden."
+
#: includes/class-m365-login-certificate.php:29
msgid "The PHP OpenSSL extension is not available."
msgstr "Die PHP-Erweiterung OpenSSL ist nicht verfügbar."
@@ -1183,30 +1211,38 @@ msgstr "Es werden nur RSA-Schlüssel unterstützt."
msgid "The RSA key must have at least 2048 bits."
msgstr "Der RSA-Schlüssel muss mindestens 2048 Bit haben."
-#: includes/class-m365-login-certificate.php:102
+#: includes/class-m365-login-certificate.php:101
+msgid "The certificate field contains a private key. Paste only the certificate (-----BEGIN CERTIFICATE-----) there."
+msgstr "Das Zertifikatsfeld enthält einen privaten Schlüssel. Fügen Sie dort nur das Zertifikat ein (-----BEGIN CERTIFICATE-----)."
+
+#: includes/class-m365-login-certificate.php:105 includes/class-m365-login-certificate.php:119
msgid "The certificate could not be read. Paste it in PEM format (-----BEGIN CERTIFICATE-----)."
msgstr "Das Zertifikat konnte nicht gelesen werden. Bitte im PEM-Format einfügen (-----BEGIN CERTIFICATE-----)."
-#: includes/class-m365-login-certificate.php:105
+#: includes/class-m365-login-certificate.php:108
msgid "The certificate does not belong to this private key."
msgstr "Das Zertifikat gehört nicht zu diesem privaten Schlüssel."
-#: includes/class-m365-login-certificate.php:110
+#: includes/class-m365-login-certificate.php:113
msgid "The certificate has already expired."
msgstr "Das Zertifikat ist bereits abgelaufen."
-#: includes/class-m365-login-graph.php:434
+#: includes/class-m365-login-graph.php:436
msgid "Group"
msgstr "Gruppe"
-#: includes/class-m365-login-graph.php:436
+#: includes/class-m365-login-graph.php:438
msgid "Security group"
msgstr "Sicherheitsgruppe"
-#: includes/class-m365-login-graph.php:438
+#: includes/class-m365-login-graph.php:440
msgid "Microsoft 365 group"
msgstr "Microsoft 365-Gruppe"
+#: includes/class-m365-login-graph.php:444
+msgid "Public Microsoft 365 group – anyone in the organisation can join"
+msgstr "Öffentliche Microsoft-365-Gruppe – jeder in der Organisation kann beitreten"
+
#: includes/class-m365-login-settings.php:55
msgid "Sign in with Microsoft"
msgstr "Login mit Microsoft"
@@ -1215,352 +1251,370 @@ msgstr "Login mit Microsoft"
msgid "or"
msgstr "oder"
-#: includes/class-m365-login-settings.php:226 includes/class-m365-login-settings.php:540
+#: includes/class-m365-login-settings.php:226 includes/class-m365-login-settings.php:543
msgid "The private key could not be encrypted. Is the OpenSSL extension available?"
msgstr "Der private Schlüssel konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login-settings.php:486
+#: includes/class-m365-login-settings.php:489
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
-#: includes/class-m365-login-settings.php:494
+#: includes/class-m365-login-settings.php:497
msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
-#: includes/class-m365-login-settings.php:506
+#: includes/class-m365-login-settings.php:509
msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen."
-#: includes/class-m365-login-settings.php:510
+#: includes/class-m365-login-settings.php:513
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
-#: includes/class-m365-login-settings.php:530
+#: includes/class-m365-login-settings.php:533
msgid "Please paste both the private key and the certificate."
msgstr "Bitte sowohl den privaten Schlüssel als auch das Zertifikat einfügen."
-#: includes/class-m365-login-settings.php:532
+#: includes/class-m365-login-settings.php:535
msgid "The pasted key or certificate is too large."
msgstr "Der eingefügte Schlüssel oder das Zertifikat ist zu groß."
-#: includes/class-m365-login-settings.php:549
+#: includes/class-m365-login-settings.php:552
msgid "Certificate authentication is selected but no certificate is stored yet. Generate one or paste your own; the Microsoft button stays hidden until then."
msgstr "Zertifikats-Authentifizierung ist ausgewählt, aber es ist noch kein Zertifikat gespeichert. Eines erzeugen oder ein eigenes einfügen; bis dahin bleibt der Microsoft-Button ausgeblendet."
-#: includes/class-m365-login-settings.php:582
+#: includes/class-m365-login-settings.php:585
msgid "The custom login page must be a URL on this site."
msgstr "Die eigene Login-Seite muss eine URL dieser Website sein."
-#: includes/class-m365-login-settings.php:689
+#: includes/class-m365-login-settings.php:692
msgid "User sync: \"Delete\" needs a user who receives the posts of deleted accounts. Until one is selected, accounts are deactivated instead."
msgstr "Benutzer-Sync: „Löschen“ braucht einen Benutzer, der die Beiträge gelöschter Konten übernimmt. Bis einer ausgewählt ist, werden Konten stattdessen deaktiviert."
-#: includes/class-m365-login-sync.php:110
+#: includes/class-m365-login-sync.php:119
msgid "Display name"
msgstr "Anzeigename"
-#: includes/class-m365-login-sync.php:114
+#: includes/class-m365-login-sync.php:123
msgid "First name"
msgstr "Vorname"
-#: includes/class-m365-login-sync.php:118
+#: includes/class-m365-login-sync.php:127
msgid "Last name"
msgstr "Nachname"
-#: includes/class-m365-login-sync.php:122
+#: includes/class-m365-login-sync.php:131
msgid "Profile photo (used as avatar)"
msgstr "Profilbild (als Avatar)"
-#: includes/class-m365-login-sync.php:126
+#: includes/class-m365-login-sync.php:135
msgid "Job title"
msgstr "Position"
-#: includes/class-m365-login-sync.php:130
+#: includes/class-m365-login-sync.php:139
msgid "Department"
msgstr "Abteilung"
-#: includes/class-m365-login-sync.php:134
+#: includes/class-m365-login-sync.php:143
msgid "Company"
msgstr "Firma"
-#: includes/class-m365-login-sync.php:138
+#: includes/class-m365-login-sync.php:147
msgid "Office"
msgstr "Büro"
-#: includes/class-m365-login-sync.php:142
+#: includes/class-m365-login-sync.php:151
msgid "Employee ID"
msgstr "Personalnummer"
-#: includes/class-m365-login-sync.php:146
+#: includes/class-m365-login-sync.php:155
msgid "Business phone"
msgstr "Telefon (geschäftlich)"
-#: includes/class-m365-login-sync.php:150
+#: includes/class-m365-login-sync.php:159
msgid "Mobile phone"
msgstr "Mobiltelefon"
-#: includes/class-m365-login-sync.php:154
+#: includes/class-m365-login-sync.php:163
msgid "Street address"
msgstr "Straße"
-#: includes/class-m365-login-sync.php:158
+#: includes/class-m365-login-sync.php:167
msgid "Postal code"
msgstr "Postleitzahl"
-#: includes/class-m365-login-sync.php:162
+#: includes/class-m365-login-sync.php:171
msgid "City"
msgstr "Ort"
-#: includes/class-m365-login-sync.php:166
+#: includes/class-m365-login-sync.php:175
msgid "State / province"
msgstr "Bundesland / Region"
-#: includes/class-m365-login-sync.php:170
+#: includes/class-m365-login-sync.php:179
msgid "Country"
msgstr "Land"
-#: includes/class-m365-login-sync.php:174
+#: includes/class-m365-login-sync.php:183
msgid "Language (sets the admin language if installed)"
msgstr "Sprache (setzt die Backend-Sprache, falls installiert)"
-#: includes/class-m365-login-sync.php:309
+#: includes/class-m365-login-sync.php:319
msgid "Another sync is still running. Please try again in a few minutes."
msgstr "Ein anderer Sync läuft noch. Bitte versuchen Sie es in ein paar Minuten erneut."
-#: includes/class-m365-login-sync.php:366
+#: includes/class-m365-login-sync.php:405
msgid "The connection to Microsoft Entra ID is not configured yet."
msgstr "Die Verbindung zu Microsoft Entra ID ist noch nicht eingerichtet."
-#: includes/class-m365-login-sync.php:370
+#: includes/class-m365-login-sync.php:409
msgid "The user sync needs a pinned tenant ID (GUID) on the Connection tab."
msgstr "Der Benutzer-Sync braucht eine feste Tenant-ID (GUID) im Tab „Verbindung“."
-#: includes/class-m365-login-sync.php:374
+#: includes/class-m365-login-sync.php:413
msgid "The default role does not exist. Please check the sync settings."
msgstr "Die Standardrolle existiert nicht. Bitte prüfen Sie die Sync-Einstellungen."
#. translators: %d: number of users
-#: includes/class-m365-login-sync.php:386
+#: includes/class-m365-login-sync.php:425
msgid "%d user read from Microsoft 365."
msgid_plural "%d users read from Microsoft 365."
msgstr[0] "%d Benutzer aus Microsoft 365 gelesen."
msgstr[1] "%d Benutzer aus Microsoft 365 gelesen."
+#: includes/class-m365-login-sync.php:436
+msgid "Microsoft 365 returned no users at all while accounts are linked. Nothing was changed. Check the tenant and the sync groups."
+msgstr "Microsoft 365 hat überhaupt keine Benutzer geliefert, obwohl Konten verknüpft sind. Es wurde nichts geändert. Prüfen Sie den Tenant und die Sync-Gruppen."
+
+#: includes/class-m365-login-sync.php:484
+msgid "The tenant ID changed since the last sync. Accounts linked in the old tenant are not found in the new one, so no account was deactivated or deleted in this run. Run the sync again to apply deactivations."
+msgstr "Die Tenant-ID hat sich seit dem letzten Sync geändert. Im alten Tenant verknüpfte Konten werden im neuen nicht gefunden, deshalb wurde in diesem Lauf kein Konto deaktiviert oder gelöscht. Starten Sie den Sync erneut, um Deaktivierungen anzuwenden."
+
#. translators: 1: number of accounts, 2: limit
-#: includes/class-m365-login-sync.php:443
+#: includes/class-m365-login-sync.php:496
msgid "Safety stop: %1$d accounts would be deactivated or deleted, more than the limit of %2$d per run. No account was deactivated or deleted. Check the sync groups and the tenant, then run the sync again (the limit can be changed with the m365_login_sync_deprovision_limit filter)."
msgstr "Sicherheitsstopp: %1$d Konten würden deaktiviert oder gelöscht, mehr als das Limit von %2$d pro Lauf. Es wurde kein Konto deaktiviert oder gelöscht. Prüfen Sie die Sync-Gruppen und den Tenant und starten Sie den Sync dann erneut (das Limit lässt sich mit dem Filter m365_login_sync_deprovision_limit ändern)."
+#: includes/class-m365-login-sync.php:621 includes/class-m365-login-sync.php:674 includes/class-m365-login-sync.php:1028 includes/class-m365-login-sync.php:1816
+msgid "disabled in Microsoft 365"
+msgstr "in Microsoft 365 deaktiviert"
+
#. translators: %s: user principal name
-#: includes/class-m365-login-sync.php:567
+#: includes/class-m365-login-sync.php:632
msgid "%s: no usable e-mail address, skipped."
msgstr "%s: keine verwendbare E-Mail-Adresse, übersprungen."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:574
+#: includes/class-m365-login-sync.php:639
msgid "%s: e-mail domain is not on the allow-list, skipped."
msgstr "%s: E-Mail-Domain steht nicht auf der Liste erlaubter Domains, übersprungen."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:586
+#: includes/class-m365-login-sync.php:651
msgid "%s: the WordPress account with this e-mail address is linked to a different Microsoft account, skipped."
msgstr "%s: Das WordPress-Konto mit dieser E-Mail-Adresse ist mit einem anderen Microsoft-Konto verknüpft, übersprungen."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:590
+#: includes/class-m365-login-sync.php:656
+msgid "%s: privileged WordPress account – it is only linked when the Microsoft user principal name equals its e-mail address (member account, no guest). Skipped."
+msgstr "%s: privilegiertes WordPress-Konto – es wird nur verknüpft, wenn der Microsoft-Benutzerprinzipalname seiner E-Mail-Adresse entspricht (Mitgliedskonto, kein Gast). Übersprungen."
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:660
msgid "%s: existing account linked."
msgstr "%s: bestehendes Konto verknüpft."
-#: includes/class-m365-login-sync.php:604 includes/class-m365-login-sync.php:955 includes/class-m365-login-sync.php:1652
-msgid "disabled in Microsoft 365"
-msgstr "in Microsoft 365 deaktiviert"
-
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:615
+#: includes/class-m365-login-sync.php:685
msgid "%s: added to this site."
msgstr "%s: zu dieser Website hinzugefügt."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:624
+#: includes/class-m365-login-sync.php:694
msgid "%s: reactivated (active in Microsoft 365 again)."
msgstr "%s: reaktiviert (in Microsoft 365 wieder aktiv)."
#. translators: 1: e-mail address, 2: list of changed fields
-#: includes/class-m365-login-sync.php:635
+#: includes/class-m365-login-sync.php:705
msgid "%1$s: updated (%2$s)."
msgstr "%1$s: aktualisiert (%2$s)."
#. translators: 1: e-mail address, 2: role names
-#: includes/class-m365-login-sync.php:661
+#: includes/class-m365-login-sync.php:731
msgid "%1$s: account created (%2$s)."
msgstr "%1$s: Konto angelegt (%2$s)."
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:688
+#: includes/class-m365-login-sync.php:758
msgid "%1$s: account could not be created: %2$s"
msgstr "%1$s: Konto konnte nicht angelegt werden: %2$s"
+#. translators: 1: current e-mail address, 2: e-mail address in Microsoft 365
+#: includes/class-m365-login-sync.php:816
+msgid "%1$s: the e-mail address in Microsoft 365 changed to %2$s. It is not changed automatically for privileged accounts – update it by hand if intended."
+msgstr "%1$s: Die E-Mail-Adresse in Microsoft 365 wurde zu %2$s geändert. Bei privilegierten Konten wird sie nicht automatisch geändert – bei Bedarf von Hand anpassen."
+
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:746
+#: includes/class-m365-login-sync.php:819
msgid "%s: e-mail address is used by another WordPress account and was not changed."
msgstr "%s: Die E-Mail-Adresse gehört bereits einem anderen WordPress-Konto und wurde nicht geändert."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:749
+#: includes/class-m365-login-sync.php:822
msgid "e-mail"
msgstr "E-Mail"
#. translators: %s: profile field
-#: includes/class-m365-login-sync.php:799
+#: includes/class-m365-login-sync.php:872
msgid "%s removed"
msgstr "%s entfernt"
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:809
+#: includes/class-m365-login-sync.php:882
msgid "%1$s: profile could not be updated: %2$s"
msgstr "%1$s: Profil konnte nicht aktualisiert werden: %2$s"
#. translators: %s: role names
-#: includes/class-m365-login-sync.php:915
+#: includes/class-m365-login-sync.php:988
msgid "roles: %s"
msgstr "Rollen: %s"
-#: includes/class-m365-login-sync.php:950 includes/class-m365-login-sync.php:1653
+#: includes/class-m365-login-sync.php:1023 includes/class-m365-login-sync.php:1817
msgid "deleted in Microsoft 365"
msgstr "in Microsoft 365 gelöscht"
-#: includes/class-m365-login-sync.php:958 includes/class-m365-login-sync.php:1654
+#: includes/class-m365-login-sync.php:1031 includes/class-m365-login-sync.php:1818
msgid "no longer a member of the sync groups"
msgstr "kein Mitglied der Sync-Gruppen mehr"
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:978
+#: includes/class-m365-login-sync.php:1063
msgid "%1$s: %2$s, but the account is protected (administrator or your own account) and was not changed."
msgstr "%1$s: %2$s, das Konto ist aber geschützt (Administrator oder Ihr eigenes Konto) und wurde nicht geändert."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1011
+#: includes/class-m365-login-sync.php:1140
msgid "%s: no valid user to receive the content is selected, so the account is deactivated instead of deleted."
msgstr "%s: Es ist kein gültiger Benutzer für die Übernahme der Inhalte ausgewählt, deshalb wird das Konto deaktiviert statt gelöscht."
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:1020
+#: includes/class-m365-login-sync.php:1149
msgid "%1$s: account deleted (%2$s)."
msgstr "%1$s: Konto gelöscht (%2$s)."
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:1029
+#: includes/class-m365-login-sync.php:1158
msgid "%1$s: account deactivated (%2$s)."
msgstr "%1$s: Konto deaktiviert (%2$s)."
-#: includes/class-m365-login-sync.php:1108
+#: includes/class-m365-login-sync.php:1237
msgid "Microsoft Graph refused the request. Grant the application permissions \"User.Read.All\" and \"GroupMember.Read.All\" with admin consent in Entra ID."
msgstr "Microsoft Graph hat die Anfrage abgelehnt. Erteilen Sie in Entra ID die Anwendungsberechtigungen „User.Read.All“ und „GroupMember.Read.All“ mit Administratorzustimmung."
#. translators: %s: error message
-#: includes/class-m365-login-sync.php:1111
+#: includes/class-m365-login-sync.php:1240
msgid "Microsoft Graph error: %s"
msgstr "Microsoft-Graph-Fehler: %s"
-#: includes/class-m365-login-sync.php:1129
+#: includes/class-m365-login-sync.php:1258
msgid "Log truncated."
msgstr "Protokoll gekürzt."
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:1205
+#: includes/class-m365-login-sync.php:1334
msgid "%1$s: profile photo could not be read: %2$s"
msgstr "%1$s: Profilbild konnte nicht gelesen werden: %2$s"
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1229 includes/class-m365-login-sync.php:1272
+#: includes/class-m365-login-sync.php:1358 includes/class-m365-login-sync.php:1401
msgid "%s: profile photo updated."
msgstr "%s: Profilbild aktualisiert."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1248
+#: includes/class-m365-login-sync.php:1377
msgid "%s: profile photo could not be downloaded."
msgstr "%s: Profilbild konnte nicht heruntergeladen werden."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1255
+#: includes/class-m365-login-sync.php:1384
msgid "%s: profile photo is not a valid image or could not be saved."
msgstr "%s: Profilbild ist kein gültiges Bild oder konnte nicht gespeichert werden."
#. translators: %d: number of photos
-#: includes/class-m365-login-sync.php:1278
+#: includes/class-m365-login-sync.php:1407
msgid "%d changed profile photo will be downloaded in the next run (download limit per run reached)."
msgid_plural "%d changed profile photos will be downloaded in the next run (download limit per run reached)."
msgstr[0] "%d geändertes Profilbild wird im nächsten Lauf heruntergeladen (Download-Limit pro Lauf erreicht)."
msgstr[1] "%d geänderte Profilbilder werden im nächsten Lauf heruntergeladen (Download-Limit pro Lauf erreicht)."
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1293
+#: includes/class-m365-login-sync.php:1422
msgid "%s: profile photo removed."
msgstr "%s: Profilbild entfernt."
#. translators: %d: number of photos
-#: includes/class-m365-login-sync.php:1321
+#: includes/class-m365-login-sync.php:1450
msgid "Profile photo sync is off: %d stored photo removed."
msgid_plural "Profile photo sync is off: %d stored photos removed."
msgstr[0] "Profilbild-Sync ist aus: %d gespeichertes Profilbild entfernt."
msgstr[1] "Profilbild-Sync ist aus: %d gespeicherte Profilbilder entfernt."
-#: includes/class-m365-login-sync.php:1549 includes/class-m365-login-sync.php:1682
+#: includes/class-m365-login-sync.php:1713 includes/class-m365-login-sync.php:1846
msgid "Microsoft 365"
msgstr "Microsoft 365"
-#: includes/class-m365-login-sync.php:1567
+#: includes/class-m365-login-sync.php:1731
msgid "Deactivated"
msgstr "Deaktiviert"
-#: includes/class-m365-login-sync.php:1570
+#: includes/class-m365-login-sync.php:1734
msgid "Imported"
msgstr "Importiert"
-#: includes/class-m365-login-sync.php:1572
+#: includes/class-m365-login-sync.php:1736
msgid "Linked"
msgstr "Verknüpft"
-#: includes/class-m365-login-sync.php:1600
+#: includes/class-m365-login-sync.php:1764
msgid "Reactivate"
msgstr "Reaktivieren"
-#: includes/class-m365-login-sync.php:1600
+#: includes/class-m365-login-sync.php:1764
msgid "Deactivate"
msgstr "Deaktivieren"
-#: includes/class-m365-login-sync.php:1633
+#: includes/class-m365-login-sync.php:1797
msgid "The account has been deactivated and signed out everywhere."
msgstr "Das Konto wurde deaktiviert und überall abgemeldet."
-#: includes/class-m365-login-sync.php:1634
+#: includes/class-m365-login-sync.php:1798
msgid "The account has been reactivated."
msgstr "Das Konto wurde reaktiviert."
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1658
+#: includes/class-m365-login-sync.php:1822
msgid "Deactivated since %1$s (%2$s)"
msgstr "Deaktiviert seit %1$s (%2$s)"
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1660
+#: includes/class-m365-login-sync.php:1824
msgid "manually"
msgstr "manuell"
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1662
+#: includes/class-m365-login-sync.php:1826
msgid "Status"
msgstr "Status"
-#: includes/class-m365-login-sync.php:1665
+#: includes/class-m365-login-sync.php:1829
msgid "Object ID"
msgstr "Objekt-ID"
-#: includes/class-m365-login-sync.php:1669
+#: includes/class-m365-login-sync.php:1833
msgid "Last sync"
msgstr "Letzter Sync"
-#: includes/class-m365-login-sync.php:1691
+#: includes/class-m365-login-sync.php:1855
msgid "These values are managed by the Microsoft 365 user sync and overwritten on the next run."
msgstr "Diese Werte verwaltet der Microsoft-365-Benutzer-Sync; sie werden beim nächsten Lauf überschrieben."
diff --git a/languages/m365-login.pot b/languages/m365-login.pot
index 2af22c3..a9072dd 100644
--- a/languages/m365-login.pot
+++ b/languages/m365-login.pot
@@ -14,1133 +14,1161 @@ msgstr ""
"X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n"
-#: includes/class-m365-login-admin.php:92 includes/class-m365-login-admin.php:93 includes/class-m365-login-admin.php:104 includes/class-m365-login-admin.php:725
+#: includes/class-m365-login-admin.php:108 includes/class-m365-login-admin.php:109 includes/class-m365-login-admin.php:120 includes/class-m365-login-admin.php:778
msgid "M365 Login"
msgstr ""
-#: includes/class-m365-login-admin.php:119
+#: includes/class-m365-login-admin.php:135
msgid "Connection"
msgstr ""
-#: includes/class-m365-login-admin.php:120
+#: includes/class-m365-login-admin.php:136
msgid "Button"
msgstr ""
-#: includes/class-m365-login-admin.php:121
+#: includes/class-m365-login-admin.php:137
msgid "Security"
msgstr ""
-#: includes/class-m365-login-admin.php:122
+#: includes/class-m365-login-admin.php:138
msgid "User sync"
msgstr ""
-#: includes/class-m365-login-admin.php:197
-msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
+#: includes/class-m365-login-admin.php:207
+msgid "M365 Login: button-only mode is on, but the connection to Microsoft is broken (missing or undecryptable secret, or expired certificate). Nobody can sign in except through the fallback link."
msgstr ""
-#: includes/class-m365-login-admin.php:199
+#: includes/class-m365-login-admin.php:209 includes/class-m365-login-admin.php:227
msgid "Open the settings"
msgstr ""
-#: includes/class-m365-login-admin.php:230
+#: includes/class-m365-login-admin.php:225
+msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:263
msgid "Choose button icon"
msgstr ""
-#: includes/class-m365-login-admin.php:231
+#: includes/class-m365-login-admin.php:264
msgid "Use this icon"
msgstr ""
-#: includes/class-m365-login-admin.php:232
+#: includes/class-m365-login-admin.php:265
msgid "Copied!"
msgstr ""
-#: includes/class-m365-login-admin.php:233 includes/class-m365-login-admin.php:848 includes/class-m365-login-admin.php:1112 includes/class-m365-login-admin.php:1157
+#: includes/class-m365-login-admin.php:266 includes/class-m365-login-admin.php:904 includes/class-m365-login-admin.php:1171 includes/class-m365-login-admin.php:1216
msgid "Copy"
msgstr ""
-#: includes/class-m365-login-admin.php:234
+#: includes/class-m365-login-admin.php:267
msgid "Testing…"
msgstr ""
-#: includes/class-m365-login-admin.php:235
+#: includes/class-m365-login-admin.php:268
msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections."
msgstr ""
-#: includes/class-m365-login-admin.php:236
+#: includes/class-m365-login-admin.php:269
msgid "No groups found."
msgstr ""
-#: includes/class-m365-login-admin.php:237
+#: includes/class-m365-login-admin.php:270
msgid "Searching…"
msgstr ""
-#: includes/class-m365-login-admin.php:238
+#: includes/class-m365-login-admin.php:271
msgid "Add"
msgstr ""
-#: includes/class-m365-login-admin.php:239 includes/class-m365-login-admin.php:495
+#: includes/class-m365-login-admin.php:272 includes/class-m365-login-admin.php:528
msgid "Remove"
msgstr ""
-#: includes/class-m365-login-admin.php:240 includes/class-m365-login-admin.php:303 includes/class-m365-login-admin.php:468
+#: includes/class-m365-login-admin.php:273 includes/class-m365-login-admin.php:336 includes/class-m365-login-admin.php:501
msgid "Save the connection settings first, then search for groups."
msgstr ""
-#: includes/class-m365-login-admin.php:241
+#: includes/class-m365-login-admin.php:274
msgid "Generate a new fallback key on save? The old link stops working."
msgstr ""
-#: includes/class-m365-login-admin.php:242
+#: includes/class-m365-login-admin.php:275
msgid "Generating a 3072-bit key pair, this takes a moment…"
msgstr ""
-#: includes/class-m365-login-admin.php:243
+#: includes/class-m365-login-admin.php:276
msgid "Replace the stored certificate? Sign-in stops working until the new certificate is uploaded to Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:244
+#: includes/class-m365-login-admin.php:277
msgid "Remove the stored certificate when saving? Sign-in with the certificate method stops working."
msgstr ""
-#: includes/class-m365-login-admin.php:245
+#: includes/class-m365-login-admin.php:278
msgid "Sync is running, this can take a while for large directories…"
msgstr ""
-#: includes/class-m365-login-admin.php:246
+#: includes/class-m365-login-admin.php:279
msgid "Run the sync now with the saved settings? Accounts are created, updated and possibly deactivated or deleted. Tip: run a dry run first."
msgstr ""
-#: includes/class-m365-login-admin.php:247
+#: includes/class-m365-login-admin.php:280
msgid "The request failed or timed out. Reload the page in a few minutes to see the report; for very large directories use \"wp m365-login sync\" (WP-CLI)."
msgstr ""
-#: includes/class-m365-login-admin.php:248
+#: includes/class-m365-login-admin.php:281
msgid "You have unsaved changes. The sync uses the saved settings – save first."
msgstr ""
-#: includes/class-m365-login-admin.php:249 includes/class-m365-login-admin.php:482
+#: includes/class-m365-login-admin.php:282 includes/class-m365-login-admin.php:515
msgid "Move up"
msgstr ""
-#: includes/class-m365-login-admin.php:261 includes/class-m365-login-admin.php:300 includes/class-m365-login-admin.php:326 includes/class-m365-login-admin.php:359 includes/class-m365-login-admin.php:514 includes/class-m365-login-sync.php:1611
+#: includes/class-m365-login-admin.php:294 includes/class-m365-login-admin.php:333 includes/class-m365-login-admin.php:359 includes/class-m365-login-admin.php:392 includes/class-m365-login-admin.php:566 includes/class-m365-login-sync.php:1775
msgid "You are not allowed to do this."
msgstr ""
-#: includes/class-m365-login-admin.php:266
+#: includes/class-m365-login-admin.php:299
msgid "Please enter a valid tenant ID first."
msgstr ""
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:280
+#: includes/class-m365-login-admin.php:313
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr ""
#. translators: %d: HTTP status code
-#: includes/class-m365-login-admin.php:289
+#: includes/class-m365-login-admin.php:322
msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr ""
-#: includes/class-m365-login-admin.php:312
+#: includes/class-m365-login-admin.php:345
msgid "Microsoft Graph refused the request. Grant the application permission \"GroupMember.Read.All\" (or \"Directory.Read.All\") with admin consent in Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:330
+#: includes/class-m365-login-admin.php:363
msgid "Unknown operation."
msgstr ""
-#: includes/class-m365-login-admin.php:347
+#: includes/class-m365-login-admin.php:380
msgid "Certificate generated and stored. Download the .cer file and upload it in Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:374
+#: includes/class-m365-login-admin.php:407
msgid "The sync has not run yet."
msgstr ""
-#: includes/class-m365-login-admin.php:378
+#: includes/class-m365-login-admin.php:411
msgid "Finished"
msgstr ""
-#: includes/class-m365-login-admin.php:379
+#: includes/class-m365-login-admin.php:412
msgid "Failed"
msgstr ""
-#: includes/class-m365-login-admin.php:380
+#: includes/class-m365-login-admin.php:413
msgid "Stopped by the safety limit"
msgstr ""
-#: includes/class-m365-login-admin.php:381
+#: includes/class-m365-login-admin.php:414
msgid "Not started"
msgstr ""
-#: includes/class-m365-login-admin.php:384
+#: includes/class-m365-login-admin.php:417
msgid "started manually"
msgstr ""
-#: includes/class-m365-login-admin.php:385
+#: includes/class-m365-login-admin.php:418
msgid "scheduled"
msgstr ""
-#: includes/class-m365-login-admin.php:386
+#: includes/class-m365-login-admin.php:419
msgid "WP-CLI"
msgstr ""
-#: includes/class-m365-login-admin.php:389
+#: includes/class-m365-login-admin.php:422
msgid "would be created"
msgstr ""
-#: includes/class-m365-login-admin.php:389
+#: includes/class-m365-login-admin.php:422
msgid "created"
msgstr ""
-#: includes/class-m365-login-admin.php:390
+#: includes/class-m365-login-admin.php:423
msgid "would be updated"
msgstr ""
-#: includes/class-m365-login-admin.php:390
+#: includes/class-m365-login-admin.php:423
msgid "updated"
msgstr ""
-#: includes/class-m365-login-admin.php:391
+#: includes/class-m365-login-admin.php:424
msgid "would be linked"
msgstr ""
-#: includes/class-m365-login-admin.php:391
+#: includes/class-m365-login-admin.php:424
msgid "linked"
msgstr ""
-#: includes/class-m365-login-admin.php:392
+#: includes/class-m365-login-admin.php:425
msgid "unchanged"
msgstr ""
-#: includes/class-m365-login-admin.php:393
+#: includes/class-m365-login-admin.php:426
msgid "would be deactivated"
msgstr ""
-#: includes/class-m365-login-admin.php:393
+#: includes/class-m365-login-admin.php:426
msgid "deactivated"
msgstr ""
-#: includes/class-m365-login-admin.php:394
+#: includes/class-m365-login-admin.php:427
msgid "would be reactivated"
msgstr ""
-#: includes/class-m365-login-admin.php:394
+#: includes/class-m365-login-admin.php:427
msgid "reactivated"
msgstr ""
-#: includes/class-m365-login-admin.php:395
+#: includes/class-m365-login-admin.php:428
msgid "would be deleted"
msgstr ""
-#: includes/class-m365-login-admin.php:395
+#: includes/class-m365-login-admin.php:428
msgid "deleted"
msgstr ""
-#: includes/class-m365-login-admin.php:396
+#: includes/class-m365-login-admin.php:429
msgid "photos"
msgstr ""
-#: includes/class-m365-login-admin.php:397
+#: includes/class-m365-login-admin.php:430
msgid "skipped"
msgstr ""
-#: includes/class-m365-login-admin.php:398
+#: includes/class-m365-login-admin.php:431
msgid "errors"
msgstr ""
-#: includes/class-m365-login-admin.php:411
+#: includes/class-m365-login-admin.php:444
msgid "Dry run – nothing was changed"
msgstr ""
#. translators: 1: date and time, 2: how the run was started, 3: duration in seconds
-#: includes/class-m365-login-admin.php:416
+#: includes/class-m365-login-admin.php:449
msgid "%1$s, %2$s, %3$d s"
msgstr ""
#. translators: %d: number of log entries
-#: includes/class-m365-login-admin.php:434
+#: includes/class-m365-login-admin.php:467
msgid "Log (%d entry)"
msgid_plural "Log (%d entries)"
msgstr[0] ""
msgstr[1] ""
-#: includes/class-m365-login-admin.php:462
+#: includes/class-m365-login-admin.php:495
msgid "Search groups"
msgstr ""
-#: includes/class-m365-login-admin.php:464
+#: includes/class-m365-login-admin.php:497
msgid "Type a group name or paste an object ID…"
msgstr ""
-#: includes/class-m365-login-admin.php:465
+#: includes/class-m365-login-admin.php:498
msgid "Search"
msgstr ""
-#: includes/class-m365-login-admin.php:470
+#: includes/class-m365-login-admin.php:503
msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
msgstr ""
-#: includes/class-m365-login-admin.php:476
+#: includes/class-m365-login-admin.php:509
msgid "Selected groups"
msgstr ""
-#: includes/class-m365-login-admin.php:488
+#: includes/class-m365-login-admin.php:521
msgid "WordPress role"
msgstr ""
-#: includes/class-m365-login-admin.php:520
+#: includes/class-m365-login-admin.php:572
msgid "No certificate is stored."
msgstr ""
-#: includes/class-m365-login-admin.php:545
+#: includes/class-m365-login-admin.php:597
msgid "Do nothing"
msgstr ""
-#: includes/class-m365-login-admin.php:546
+#: includes/class-m365-login-admin.php:598
msgid "Deactivate the WordPress account"
msgstr ""
-#: includes/class-m365-login-admin.php:547
+#: includes/class-m365-login-admin.php:599
msgid "Delete the WordPress account"
msgstr ""
-#: includes/class-m365-login-admin.php:550
+#: includes/class-m365-login-admin.php:602
msgid "Account disabled in Microsoft 365 (sign-in blocked)"
msgstr ""
-#: includes/class-m365-login-admin.php:551
+#: includes/class-m365-login-admin.php:603
msgid "Account deleted in Microsoft 365"
msgstr ""
-#: includes/class-m365-login-admin.php:552
+#: includes/class-m365-login-admin.php:604
msgid "No longer a member of the sync groups"
msgstr ""
-#: includes/class-m365-login-admin.php:557
+#: includes/class-m365-login-admin.php:609
msgid "Import users from Microsoft 365"
msgstr ""
-#: includes/class-m365-login-admin.php:558
+#: includes/class-m365-login-admin.php:610
msgid "Creates a WordPress account for every Microsoft 365 user in scope, links existing accounts by e-mail address, keeps roles and profile fields up to date and deactivates or deletes accounts that were disabled or removed in Microsoft 365. New accounts get a random password and no e-mail; people sign in with the Microsoft button."
msgstr ""
-#: includes/class-m365-login-admin.php:563
+#: includes/class-m365-login-admin.php:615
msgid "Run the sync automatically"
msgstr ""
-#: includes/class-m365-login-admin.php:564
+#: includes/class-m365-login-admin.php:616
msgid "Uses WP-Cron, which runs when the site receives visits. For exact timing, trigger wp-cron.php from a real cron job or run \"wp m365-login sync\"."
msgstr ""
-#: includes/class-m365-login-admin.php:570
+#: includes/class-m365-login-admin.php:622
msgid "Interval"
msgstr ""
-#: includes/class-m365-login-admin.php:572
+#: includes/class-m365-login-admin.php:624
msgid "Hourly"
msgstr ""
-#: includes/class-m365-login-admin.php:573
+#: includes/class-m365-login-admin.php:625
msgid "Twice daily"
msgstr ""
-#: includes/class-m365-login-admin.php:574
+#: includes/class-m365-login-admin.php:626
msgid "Daily"
msgstr ""
#. translators: %s: date and time
-#: includes/class-m365-login-admin.php:578
+#: includes/class-m365-login-admin.php:630
msgid "Next run: %s"
msgstr ""
-#: includes/class-m365-login-admin.php:586
+#: includes/class-m365-login-admin.php:638
msgid "Also import guest users (B2B)"
msgstr ""
-#: includes/class-m365-login-admin.php:587
+#: includes/class-m365-login-admin.php:639
msgid "Guests are external people invited into your tenant. Off by default."
msgstr ""
-#: includes/class-m365-login-admin.php:591
+#: includes/class-m365-login-admin.php:643
msgid "Which users? (optional)"
msgstr ""
-#: includes/class-m365-login-admin.php:592
+#: includes/class-m365-login-admin.php:644
msgid "Limit the import to members of these groups (nested memberships count). Without groups, every user of the tenant is imported. The e-mail domain allow-list on the Security tab applies as well."
msgstr ""
-#: includes/class-m365-login-admin.php:593
+#: includes/class-m365-login-admin.php:645
msgid "No groups selected – all users of the tenant are imported."
msgstr ""
-#: includes/class-m365-login-admin.php:597
+#: includes/class-m365-login-admin.php:649
msgid "Roles"
msgstr ""
-#: includes/class-m365-login-admin.php:600
+#: includes/class-m365-login-admin.php:652
msgid "Default role"
msgstr ""
-#: includes/class-m365-login-admin.php:604
+#: includes/class-m365-login-admin.php:656
msgid "Every imported user gets this role. The sync manages the roles of imported accounts – manual role changes are overwritten on the next run."
msgstr ""
-#: includes/class-m365-login-admin.php:607
+#: includes/class-m365-login-admin.php:659
msgid "Additional roles from Microsoft 365 groups"
msgstr ""
-#: includes/class-m365-login-admin.php:608
+#: includes/class-m365-login-admin.php:660
msgid "Members of a group (nested memberships count) get the role next to it. If a person leaves the group, the role is removed again on the next sync."
msgstr ""
-#: includes/class-m365-login-admin.php:609
+#: includes/class-m365-login-admin.php:661
+msgid "Whoever can change a group's members controls the mapped role. For roles with administrative rights use security groups (ideally role-assignable ones) – never public Microsoft 365 groups or Teams, which members can join themselves."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:662
msgid "No group mapping – everybody gets the default role."
msgstr ""
-#: includes/class-m365-login-admin.php:612
+#: includes/class-m365-login-admin.php:665
msgid "How are mapped roles applied?"
msgstr ""
-#: includes/class-m365-login-admin.php:615
+#: includes/class-m365-login-admin.php:668
msgid "In addition to the default role (a user can have several roles)"
msgstr ""
-#: includes/class-m365-login-admin.php:619
+#: includes/class-m365-login-admin.php:672
msgid "Instead of the default role – the first matching group in the list wins (use ↑ to reorder)"
msgstr ""
-#: includes/class-m365-login-admin.php:626
+#: includes/class-m365-login-admin.php:679
msgid "Also manage the roles of accounts that existed before the sync"
msgstr ""
-#: includes/class-m365-login-admin.php:627
+#: includes/class-m365-login-admin.php:680
msgid "Off: existing accounts are only linked and get their profile fields updated; their roles stay as they are. Administrators that existed before the sync and your own account are never changed."
msgstr ""
-#: includes/class-m365-login-admin.php:633
+#: includes/class-m365-login-admin.php:686
msgid "Profile fields"
msgstr ""
-#: includes/class-m365-login-admin.php:634
+#: includes/class-m365-login-admin.php:687
msgid "Selected Microsoft 365 attributes are copied into the WordPress profile on every sync (Microsoft 365 wins). Name fields go into the standard profile fields, everything else into user meta keys starting with \"m365_\" – usable by themes and other plugins – and is shown on the profile screen."
msgstr ""
-#: includes/class-m365-login-admin.php:643
+#: includes/class-m365-login-admin.php:696
msgid "Profile photos are stored in wp-content/uploads/m365-login-avatars/ and replace the Gravatar. They are compared on every run: changed photos are downloaded again, photos deleted in Microsoft 365 are deleted in WordPress too. Fields and photos you deselect here are removed from the profiles on the next run (first and last name and display name stay)."
msgstr ""
-#: includes/class-m365-login-admin.php:647
+#: includes/class-m365-login-admin.php:700
msgid "Disabled and deleted Microsoft 365 accounts"
msgstr ""
-#: includes/class-m365-login-admin.php:648
+#: includes/class-m365-login-admin.php:701
msgid "Applies to WordPress accounts linked to a Microsoft account (imported, or signed in with Microsoft at least once). Deactivated accounts cannot sign in at all – not with Microsoft, a password or an application password – and are signed out immediately. When the person is active in Microsoft 365 again, the sync reactivates the account."
msgstr ""
-#: includes/class-m365-login-admin.php:659
+#: includes/class-m365-login-admin.php:712
msgid "Only relevant when the import is limited to groups."
msgstr ""
-#: includes/class-m365-login-admin.php:665
+#: includes/class-m365-login-admin.php:718
msgid "Posts of deleted accounts go to"
msgstr ""
-#: includes/class-m365-login-admin.php:673
+#: includes/class-m365-login-admin.php:726
msgid "— Select a user —"
msgstr ""
-#: includes/class-m365-login-admin.php:680
+#: includes/class-m365-login-admin.php:733
msgid "Required for \"Delete\". Without a user, accounts are deactivated instead, so no content is ever lost."
msgstr ""
-#: includes/class-m365-login-admin.php:683
+#: includes/class-m365-login-admin.php:736
msgid "Safety stop: if a run would deactivate or delete more than 20 % of the linked accounts (at least 5), nothing is deactivated or deleted and the run is reported as stopped. A failed Microsoft Graph request also stops the run before anything is deactivated."
msgstr ""
-#: includes/class-m365-login-admin.php:687
+#: includes/class-m365-login-admin.php:740
msgid "Run the sync"
msgstr ""
-#: includes/class-m365-login-admin.php:688
+#: includes/class-m365-login-admin.php:741
msgid "The run uses the saved settings. Start with a dry run: it reads Microsoft 365 and lists what would change, without changing anything."
msgstr ""
-#: includes/class-m365-login-admin.php:690
+#: includes/class-m365-login-admin.php:743
msgid "Dry run"
msgstr ""
-#: includes/class-m365-login-admin.php:691
+#: includes/class-m365-login-admin.php:744
msgid "Sync now"
msgstr ""
-#: includes/class-m365-login-admin.php:693
+#: includes/class-m365-login-admin.php:746
msgid "Required application permissions (Microsoft Graph, admin consent): User.Read.All, and GroupMember.Read.All when groups are used."
msgstr ""
-#: includes/class-m365-login-admin.php:707
+#: includes/class-m365-login-admin.php:760
msgid "You are not allowed to access this page."
msgstr ""
-#: includes/class-m365-login-admin.php:726
+#: includes/class-m365-login-admin.php:779
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr ""
-#: includes/class-m365-login-admin.php:731
+#: includes/class-m365-login-admin.php:784
msgid "Connected"
msgstr ""
-#: includes/class-m365-login-admin.php:731
+#: includes/class-m365-login-admin.php:784
msgid "Setup incomplete"
msgstr ""
-#: includes/class-m365-login-admin.php:753
+#: includes/class-m365-login-admin.php:806
msgid "Microsoft Entra ID app registration"
msgstr ""
-#: includes/class-m365-login-admin.php:754
+#: includes/class-m365-login-admin.php:807
msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr ""
-#: includes/class-m365-login-admin.php:757
+#: includes/class-m365-login-admin.php:810
msgid "Directory (tenant) ID"
msgstr ""
-#: includes/class-m365-login-admin.php:760
+#: includes/class-m365-login-admin.php:813
msgid "Test tenant"
msgstr ""
-#: includes/class-m365-login-admin.php:762
+#: includes/class-m365-login-admin.php:815
msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account."
msgstr ""
-#: includes/class-m365-login-admin.php:764
+#: includes/class-m365-login-admin.php:817
msgid "Multi-tenant mode: accounts from any Microsoft tenant can sign in. Their \"email\" attribute is not verified, so the plugin matches on the user principal name (verified domain) only and ignores the e-mail claim unless Microsoft marks it as domain-verified. Use the e-mail domain allow-list on the Security tab, or better, pin your tenant GUID."
msgstr ""
-#: includes/class-m365-login-admin.php:770
+#: includes/class-m365-login-admin.php:823
msgid "Application (client) ID"
msgstr ""
-#: includes/class-m365-login-admin.php:775
+#: includes/class-m365-login-admin.php:828
msgid "How should WordPress authenticate to Microsoft?"
msgstr ""
-#: includes/class-m365-login-admin.php:780 includes/class-m365-login-admin.php:797
+#: includes/class-m365-login-admin.php:833 includes/class-m365-login-admin.php:850
msgid "Client secret"
msgstr ""
-#: includes/class-m365-login-admin.php:781
+#: includes/class-m365-login-admin.php:834
msgid "Quick to set up. A password-like value created in Entra ID that expires after 6–24 months and must be renewed."
msgstr ""
-#: includes/class-m365-login-admin.php:787
+#: includes/class-m365-login-admin.php:840
msgid "Certificate"
msgstr ""
-#: includes/class-m365-login-admin.php:787
+#: includes/class-m365-login-admin.php:840
msgid "Recommended"
msgstr ""
-#: includes/class-m365-login-admin.php:788
+#: includes/class-m365-login-admin.php:841
msgid "The private key never leaves this server; only the public certificate is uploaded to Entra ID. Generated here with one click, valid for 2 years."
msgstr ""
-#: includes/class-m365-login-admin.php:799
+#: includes/class-m365-login-admin.php:852
msgid "•••••••••••• (stored, leave empty to keep)"
msgstr ""
-#: includes/class-m365-login-admin.php:799
+#: includes/class-m365-login-admin.php:852
msgid "Paste the secret value"
msgstr ""
-#: includes/class-m365-login-admin.php:800
+#: includes/class-m365-login-admin.php:853
msgid "Show secret"
msgstr ""
-#: includes/class-m365-login-admin.php:805
+#: includes/class-m365-login-admin.php:858
msgid "Remove the stored secret"
msgstr ""
-#: includes/class-m365-login-admin.php:808
+#: includes/class-m365-login-admin.php:861
msgid "Stored encrypted (AES-256-GCM, key derived from your WordPress salts) and never displayed again. Client secrets expire – note the expiry date in Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:812
+#: includes/class-m365-login-admin.php:863
+msgid "AUTH_KEY and SECURE_AUTH_KEY are not defined in wp-config.php, so WordPress keeps its salts in the database – right next to the encrypted secret. Add the salts to wp-config.php to make the encryption effective."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:868
msgid "Step-by-step: create a client secret in Entra ID"
msgstr ""
-#: includes/class-m365-login-admin.php:815
+#: includes/class-m365-login-admin.php:871
msgid "Open entra.microsoft.com and sign in with an account that has the \"Application Administrator\" or \"Global Administrator\" role."
msgstr ""
-#: includes/class-m365-login-admin.php:816
+#: includes/class-m365-login-admin.php:872
msgid "Go to Identity → Applications → App registrations and open your app (or create it first, see the general guide in the sidebar)."
msgstr ""
-#: includes/class-m365-login-admin.php:817
+#: includes/class-m365-login-admin.php:873
msgid "In the left menu choose Certificates & secrets, then the tab Client secrets, and click New client secret."
msgstr ""
-#: includes/class-m365-login-admin.php:818
+#: includes/class-m365-login-admin.php:874
msgid "Enter a description such as \"WordPress login\" and pick an expiry. Microsoft allows at most 24 months; put a reminder in your calendar two weeks before."
msgstr ""
-#: includes/class-m365-login-admin.php:819
+#: includes/class-m365-login-admin.php:875
msgid "Click Add. Copy the Value column immediately – it is shown only once. The Secret ID column is NOT what you need."
msgstr ""
-#: includes/class-m365-login-admin.php:820
+#: includes/class-m365-login-admin.php:876
msgid "Paste the value into the Client secret field above and save this page."
msgstr ""
-#: includes/class-m365-login-admin.php:822
+#: includes/class-m365-login-admin.php:878
msgid "When the secret expires, sign-ins fail with \"Could not complete the sign-in with Microsoft\". Create a new secret, paste it here, save, then delete the old one in Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:835
+#: includes/class-m365-login-admin.php:891
msgid "Expired"
msgstr ""
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:839
+#: includes/class-m365-login-admin.php:895
msgid "Expires in %d days"
msgstr ""
#. translators: %d: number of days
-#: includes/class-m365-login-admin.php:842
+#: includes/class-m365-login-admin.php:898
msgid "Valid"
msgstr ""
-#: includes/class-m365-login-admin.php:847
+#: includes/class-m365-login-admin.php:903
msgid "Thumbprint (SHA-1)"
msgstr ""
-#: includes/class-m365-login-admin.php:849
+#: includes/class-m365-login-admin.php:905
msgid "Subject"
msgstr ""
-#: includes/class-m365-login-admin.php:851
+#: includes/class-m365-login-admin.php:907
msgid "Key size"
msgstr ""
-#: includes/class-m365-login-admin.php:853
+#: includes/class-m365-login-admin.php:909
msgid "Valid until"
msgstr ""
-#: includes/class-m365-login-admin.php:857
+#: includes/class-m365-login-admin.php:913
msgid "Download certificate (.cer)"
msgstr ""
-#: includes/class-m365-login-admin.php:858
+#: includes/class-m365-login-admin.php:914
msgid "Generate new certificate"
msgstr ""
-#: includes/class-m365-login-admin.php:861
+#: includes/class-m365-login-admin.php:917
msgid "Remove certificate when saving"
msgstr ""
-#: includes/class-m365-login-admin.php:865
+#: includes/class-m365-login-admin.php:921
msgid "No certificate stored yet."
msgstr ""
-#: includes/class-m365-login-admin.php:867
+#: includes/class-m365-login-admin.php:923
msgid "Generate certificate"
msgstr ""
-#: includes/class-m365-login-admin.php:868
+#: includes/class-m365-login-admin.php:924
msgid "3072-bit RSA, self-signed, valid for 2 years. The private key is stored encrypted and never shown or downloadable."
msgstr ""
-#: includes/class-m365-login-admin.php:872
+#: includes/class-m365-login-admin.php:928
msgid "Use your own certificate instead (paste PEM)"
msgstr ""
-#: includes/class-m365-login-admin.php:875
+#: includes/class-m365-login-admin.php:931
msgid "Private key (PEM, unencrypted)"
msgstr ""
-#: includes/class-m365-login-admin.php:879
+#: includes/class-m365-login-admin.php:935
msgid "Certificate (PEM)"
msgstr ""
-#: includes/class-m365-login-admin.php:881
+#: includes/class-m365-login-admin.php:937
msgid "RSA, at least 2048 bits. The pair is validated and the key is encrypted when you save. Both fields stay empty afterwards."
msgstr ""
-#: includes/class-m365-login-admin.php:887
+#: includes/class-m365-login-admin.php:943
msgid "Step-by-step: register the certificate in Entra ID"
msgstr ""
-#: includes/class-m365-login-admin.php:890
+#: includes/class-m365-login-admin.php:946
msgid "Click Generate certificate above (or paste your own). Then click Download certificate (.cer) – the file contains only the public part."
msgstr ""
-#: includes/class-m365-login-admin.php:891
+#: includes/class-m365-login-admin.php:947
msgid "Open entra.microsoft.com → Identity → Applications → App registrations and open your app."
msgstr ""
-#: includes/class-m365-login-admin.php:892
+#: includes/class-m365-login-admin.php:948
msgid "Choose Certificates & secrets in the left menu, then the tab Certificates, and click Upload certificate."
msgstr ""
-#: includes/class-m365-login-admin.php:893
+#: includes/class-m365-login-admin.php:949
msgid "Select the downloaded .cer file, add a description such as \"WordPress login\" and click Add."
msgstr ""
-#: includes/class-m365-login-admin.php:894
+#: includes/class-m365-login-admin.php:950
msgid "Compare the thumbprint Entra ID shows with the thumbprint above – they must match exactly."
msgstr ""
-#: includes/class-m365-login-admin.php:895
+#: includes/class-m365-login-admin.php:951
msgid "Make sure Certificate is selected above and save this page. If a client secret was stored before, you may delete it in Entra ID now."
msgstr ""
-#: includes/class-m365-login-admin.php:897
+#: includes/class-m365-login-admin.php:953
msgid "How it works: for every token request WordPress signs a short-lived JWT (client assertion) with the private key; Microsoft verifies it with the uploaded certificate. Nothing secret is ever transmitted."
msgstr ""
-#: includes/class-m365-login-admin.php:898
+#: includes/class-m365-login-admin.php:954
msgid "Before the certificate expires: generate a new one here, upload it to Entra ID (both may be registered at the same time), save, then remove the old one from Entra ID. Sign-ins keep working during the switch."
msgstr ""
-#: includes/class-m365-login-admin.php:904
+#: includes/class-m365-login-admin.php:960
msgid "Account prompt"
msgstr ""
-#: includes/class-m365-login-admin.php:906
+#: includes/class-m365-login-admin.php:962
msgid "Always let the user pick an account (recommended)"
msgstr ""
-#: includes/class-m365-login-admin.php:907
+#: includes/class-m365-login-admin.php:963
msgid "Use the current Microsoft session if available"
msgstr ""
-#: includes/class-m365-login-admin.php:908
+#: includes/class-m365-login-admin.php:964
msgid "Always require re-entering credentials"
msgstr ""
-#: includes/class-m365-login-admin.php:917
+#: includes/class-m365-login-admin.php:973
msgid "Appearance"
msgstr ""
-#: includes/class-m365-login-admin.php:920
+#: includes/class-m365-login-admin.php:976
msgid "Live preview"
msgstr ""
-#: includes/class-m365-login-admin.php:934
+#: includes/class-m365-login-admin.php:990
msgid "Button text"
msgstr ""
-#: includes/class-m365-login-admin.php:938
+#: includes/class-m365-login-admin.php:994
msgid "Divider text"
msgstr ""
-#: includes/class-m365-login-admin.php:940
+#: includes/class-m365-login-admin.php:996
msgid "Leave empty to hide the divider line."
msgstr ""
-#: includes/class-m365-login-admin.php:945
+#: includes/class-m365-login-admin.php:1001
msgid "Icon"
msgstr ""
-#: includes/class-m365-login-admin.php:948
+#: includes/class-m365-login-admin.php:1004
msgid "Show an icon on the button"
msgstr ""
-#: includes/class-m365-login-admin.php:959
+#: includes/class-m365-login-admin.php:1015
msgid "Default: Microsoft logo"
msgstr ""
-#: includes/class-m365-login-admin.php:961
+#: includes/class-m365-login-admin.php:1017
msgid "Choose from media library"
msgstr ""
-#: includes/class-m365-login-admin.php:962
+#: includes/class-m365-login-admin.php:1018
msgid "Use Microsoft logo"
msgstr ""
-#: includes/class-m365-login-admin.php:964
+#: includes/class-m365-login-admin.php:1020
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
msgstr ""
-#: includes/class-m365-login-admin.php:972
+#: includes/class-m365-login-admin.php:1028
msgid "Background"
msgstr ""
-#: includes/class-m365-login-admin.php:973
+#: includes/class-m365-login-admin.php:1029
msgid "Background (hover)"
msgstr ""
-#: includes/class-m365-login-admin.php:974
+#: includes/class-m365-login-admin.php:1030
msgid "Text colour"
msgstr ""
-#: includes/class-m365-login-admin.php:975
+#: includes/class-m365-login-admin.php:1031
msgid "Border"
msgstr ""
-#: includes/class-m365-login-admin.php:988
+#: includes/class-m365-login-admin.php:1044
msgid "Corner radius"
msgstr ""
-#: includes/class-m365-login-admin.php:992
+#: includes/class-m365-login-admin.php:1048
msgid "Position on the login page"
msgstr ""
-#: includes/class-m365-login-admin.php:994
+#: includes/class-m365-login-admin.php:1050
msgid "Below the login form"
msgstr ""
-#: includes/class-m365-login-admin.php:995
+#: includes/class-m365-login-admin.php:1051
msgid "Above the login form"
msgstr ""
-#: includes/class-m365-login-admin.php:1001
+#: includes/class-m365-login-admin.php:1057
msgid "Quick presets"
msgstr ""
-#: includes/class-m365-login-admin.php:1002
+#: includes/class-m365-login-admin.php:1058
msgid "Microsoft dark"
msgstr ""
-#: includes/class-m365-login-admin.php:1003
+#: includes/class-m365-login-admin.php:1059
msgid "Microsoft light"
msgstr ""
-#: includes/class-m365-login-admin.php:1004
+#: includes/class-m365-login-admin.php:1060
msgid "Azure blue"
msgstr ""
-#: includes/class-m365-login-admin.php:1005
+#: includes/class-m365-login-admin.php:1061
msgid "WordPress blue"
msgstr ""
-#: includes/class-m365-login-admin.php:1009
+#: includes/class-m365-login-admin.php:1065
msgid "Custom login page"
msgstr ""
-#: includes/class-m365-login-admin.php:1010
+#: includes/class-m365-login-admin.php:1066
msgid "Using your own login page instead of wp-login.php? Tell the plugin where it is so error messages, the fallback link and the post-logout redirect point there."
msgstr ""
-#: includes/class-m365-login-admin.php:1013
+#: includes/class-m365-login-admin.php:1069
msgid "URL of your login page"
msgstr ""
-#: includes/class-m365-login-admin.php:1015
+#: includes/class-m365-login-admin.php:1071
msgid "Must be on this site. Leave empty to use wp-login.php."
msgstr ""
-#: includes/class-m365-login-admin.php:1021
+#: includes/class-m365-login-admin.php:1077
msgid "Add the button to every wp_login_form() form automatically"
msgstr ""
-#: includes/class-m365-login-admin.php:1022
+#: includes/class-m365-login-admin.php:1078
msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below."
msgstr ""
-#: includes/class-m365-login-admin.php:1027
+#: includes/class-m365-login-admin.php:1083
msgid "Manual placement"
msgstr ""
-#: includes/class-m365-login-admin.php:1028
+#: includes/class-m365-login-admin.php:1084
msgid "Shortcode (block editor, page builders):"
msgstr ""
-#: includes/class-m365-login-admin.php:1030
+#: includes/class-m365-login-admin.php:1086
msgid "Template function (theme files):"
msgstr ""
-#: includes/class-m365-login-admin.php:1032
+#: includes/class-m365-login-admin.php:1088
msgid "Both show the error messages of the last attempt; use m365_login_messages() to place them separately."
msgstr ""
-#: includes/class-m365-login-admin.php:1040
+#: includes/class-m365-login-admin.php:1096
msgid "User matching & hardening"
msgstr ""
-#: includes/class-m365-login-admin.php:1041
+#: includes/class-m365-login-admin.php:1097
msgid "Sign-in never creates users. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists – created by hand or imported by the user sync."
msgstr ""
-#: includes/class-m365-login-admin.php:1046
+#: includes/class-m365-login-admin.php:1102
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr ""
-#: includes/class-m365-login-admin.php:1047
+#: includes/class-m365-login-admin.php:1103
msgid "On first sign-in the immutable Microsoft object ID is stored with the user. Later sign-ins with the same e-mail but a different Microsoft identity are rejected. Strongly recommended."
msgstr ""
-#: includes/class-m365-login-admin.php:1054
+#: includes/class-m365-login-admin.php:1110
msgid "Fall back to the user principal name (UPN)"
msgstr ""
-#: includes/class-m365-login-admin.php:1055
+#: includes/class-m365-login-admin.php:1111
msgid "If the token contains no \"email\" claim, use the UPN (e.g. jane@contoso.com) when it is a valid e-mail address. Usually required for work accounts."
msgstr ""
-#: includes/class-m365-login-admin.php:1062
+#: includes/class-m365-login-admin.php:1118
msgid "Keep users signed in (\"Remember me\")"
msgstr ""
-#: includes/class-m365-login-admin.php:1063
+#: includes/class-m365-login-admin.php:1119
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr ""
-#: includes/class-m365-login-admin.php:1068
+#: includes/class-m365-login-admin.php:1124
msgid "Allowed e-mail domains (optional)"
msgstr ""
-#: includes/class-m365-login-admin.php:1070
+#: includes/class-m365-login-admin.php:1126
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr ""
-#: includes/class-m365-login-admin.php:1075
+#: includes/class-m365-login-admin.php:1131
msgid "Allowed Entra groups (optional)"
msgstr ""
-#: includes/class-m365-login-admin.php:1076
+#: includes/class-m365-login-admin.php:1132
msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
msgstr ""
-#: includes/class-m365-login-admin.php:1078
+#: includes/class-m365-login-admin.php:1134
msgid "No groups selected – every matched user may sign in."
msgstr ""
-#: includes/class-m365-login-admin.php:1080
+#: includes/class-m365-login-admin.php:1136
msgid "Membership is read from the \"groups\" claim of the ID token when present; otherwise the plugin asks Microsoft Graph (application permission \"User.Read.All\" or \"Directory.Read.All\"). If neither works, the sign-in is refused."
msgstr ""
-#: includes/class-m365-login-admin.php:1085
+#: includes/class-m365-login-admin.php:1141
msgid "Excluded Entra groups (optional)"
msgstr ""
-#: includes/class-m365-login-admin.php:1086
+#: includes/class-m365-login-admin.php:1142
msgid "Members of these groups can never sign in with Microsoft – even if they are in an allowed group. Nested memberships count."
msgstr ""
-#: includes/class-m365-login-admin.php:1088
+#: includes/class-m365-login-admin.php:1145
+msgid "Group rules need a pinned tenant ID (GUID) on the Connection tab. In multi-tenant mode the group check cannot ask Microsoft Graph, so every sign-in is refused while groups are selected here or above."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:1147
msgid "No groups excluded."
msgstr ""
-#: includes/class-m365-login-admin.php:1090
+#: includes/class-m365-login-admin.php:1149
msgid "The plugin asks Microsoft Graph on every sign-in (application permission \"User.Read.All\" or \"Directory.Read.All\"), because a \"groups\" claim may be filtered and cannot prove that someone is not a member. If the check fails, the sign-in is refused. Password sign-in is not affected – combine with button-only mode if needed."
msgstr ""
-#: includes/class-m365-login-admin.php:1095
+#: includes/class-m365-login-admin.php:1154
msgid "Button-only mode"
msgstr ""
-#: includes/class-m365-login-admin.php:1096
-msgid "Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every interactive password sign-in on the site, including custom login forms. Application passwords, REST, XML-RPC and WP-CLI are not affected."
+#: includes/class-m365-login-admin.php:1155
+msgid "Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every sign-in with a normal password on the site – custom login forms, XML-RPC and login endpoints of other plugins included. Application passwords (REST, XML-RPC) and WP-CLI keep working; API requests never receive a login cookie."
msgstr ""
-#: includes/class-m365-login-admin.php:1101
+#: includes/class-m365-login-admin.php:1160
msgid "Show only the Microsoft button on the login page"
msgstr ""
-#: includes/class-m365-login-admin.php:1102
+#: includes/class-m365-login-admin.php:1161
msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
msgstr ""
-#: includes/class-m365-login-admin.php:1107
+#: includes/class-m365-login-admin.php:1166
msgid "Fallback link (keep it secret)"
msgstr ""
-#: includes/class-m365-login-admin.php:1108
+#: includes/class-m365-login-admin.php:1167
msgid "Opening this link shows the password form again in that browser for 30 minutes and allows password sign-in there. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks."
msgstr ""
-#: includes/class-m365-login-admin.php:1116
+#: includes/class-m365-login-admin.php:1175
msgid "Generate a new key when saving"
msgstr ""
-#: includes/class-m365-login-admin.php:1119
+#: includes/class-m365-login-admin.php:1178
msgid "A key is generated automatically the first time you save these settings."
msgstr ""
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:1125
+#: includes/class-m365-login-admin.php:1184
msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
msgstr ""
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:1134
+#: includes/class-m365-login-admin.php:1193
msgid "What the plugin does to keep sign-ins safe"
msgstr ""
-#: includes/class-m365-login-admin.php:1136
+#: includes/class-m365-login-admin.php:1195
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr ""
-#: includes/class-m365-login-admin.php:1137
+#: includes/class-m365-login-admin.php:1196
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr ""
-#: includes/class-m365-login-admin.php:1138
+#: includes/class-m365-login-admin.php:1197
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr ""
-#: includes/class-m365-login-admin.php:1139
+#: includes/class-m365-login-admin.php:1198
msgid "Client secret encrypted at rest; sign-in never creates accounts or changes passwords."
msgstr ""
-#: includes/class-m365-login-admin.php:1147
+#: includes/class-m365-login-admin.php:1206
msgid "Save changes"
msgstr ""
-#: includes/class-m365-login-admin.php:1153
+#: includes/class-m365-login-admin.php:1212
msgid "Redirect URI"
msgstr ""
-#: includes/class-m365-login-admin.php:1154
+#: includes/class-m365-login-admin.php:1213
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr ""
-#: includes/class-m365-login-admin.php:1160
+#: includes/class-m365-login-admin.php:1219
msgid "Plain permalinks are active, so the callback uses a query string. If you enable pretty permalinks later, the redirect URI changes and must be updated in Entra ID."
msgstr ""
-#: includes/class-m365-login-admin.php:1163
+#: includes/class-m365-login-admin.php:1222
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr ""
-#: includes/class-m365-login-admin.php:1168
+#: includes/class-m365-login-admin.php:1227
msgid "Setup guide: app registration"
msgstr ""
-#: includes/class-m365-login-admin.php:1170
+#: includes/class-m365-login-admin.php:1229
msgid "Open entra.microsoft.com → Identity → Applications → App registrations → New registration."
msgstr ""
-#: includes/class-m365-login-admin.php:1171
+#: includes/class-m365-login-admin.php:1230
msgid "Name: e.g. \"WordPress login\". Supported account types: \"Accounts in this organizational directory only\" (single tenant)."
msgstr ""
-#: includes/class-m365-login-admin.php:1172
+#: includes/class-m365-login-admin.php:1231
msgid "Redirect URI: choose the platform Web and paste the URI shown above. Then click Register."
msgstr ""
-#: includes/class-m365-login-admin.php:1173
+#: includes/class-m365-login-admin.php:1232
msgid "On the Overview page copy the Application (client) ID and the Directory (tenant) ID into the Connection tab."
msgstr ""
-#: includes/class-m365-login-admin.php:1174
+#: includes/class-m365-login-admin.php:1233
msgid "Authentication: leave \"ID tokens\" unchecked (the plugin uses the authorization code flow) and \"Allow public client flows\" on No."
msgstr ""
-#: includes/class-m365-login-admin.php:1175
+#: includes/class-m365-login-admin.php:1234
msgid "Token configuration → Add optional claim → ID → tick \"email\" → Add. Confirm the API permission prompt."
msgstr ""
-#: includes/class-m365-login-admin.php:1176
+#: includes/class-m365-login-admin.php:1235
msgid "Pick the authentication method on the Connection tab and follow its step-by-step guide (client secret or certificate)."
msgstr ""
-#: includes/class-m365-login-admin.php:1177
+#: includes/class-m365-login-admin.php:1236
msgid "Optional: restrict who may use the app under Enterprise applications → your app → Properties → \"Assignment required\" = Yes, then assign users/groups."
msgstr ""
-#: includes/class-m365-login-admin.php:1179
+#: includes/class-m365-login-admin.php:1238
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr ""
-#: includes/class-m365-login-admin.php:1180
+#: includes/class-m365-login-admin.php:1239
msgid "Optional, for group restrictions and the user sync: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
msgstr ""
-#: includes/class-m365-login-admin.php:1184
+#: includes/class-m365-login-admin.php:1243
msgid "Shortcode"
msgstr ""
-#: includes/class-m365-login-admin.php:1185
+#: includes/class-m365-login-admin.php:1244
msgid "Place the button on a custom login page:"
msgstr ""
-#: includes/class-m365-login-admin.php:1187
+#: includes/class-m365-login-admin.php:1246
msgid "More options on the Button tab under \"Custom login page\"."
msgstr ""
-#: includes/class-m365-login-auth.php:173
+#: includes/class-m365-login-auth.php:182
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr ""
-#: includes/class-m365-login-auth.php:906
+#: includes/class-m365-login-auth.php:998
+msgid "Microsoft sign-in is temporarily unavailable. Please contact an administrator."
+msgstr ""
+
+#: includes/class-m365-login-auth.php:1005
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr ""
-#: includes/class-m365-login-auth.php:927 includes/class-m365-login-graph.php:63
+#: includes/class-m365-login-auth.php:1026 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr ""
-#: includes/class-m365-login-auth.php:928
+#: includes/class-m365-login-auth.php:1027
msgid "The login request expired or was invalid. Please try again."
msgstr ""
-#: includes/class-m365-login-auth.php:929
+#: includes/class-m365-login-auth.php:1028
msgid "Microsoft sign-in was cancelled."
msgstr ""
-#: includes/class-m365-login-auth.php:930
+#: includes/class-m365-login-auth.php:1029
msgid "Microsoft returned an error. Please try again."
msgstr ""
-#: includes/class-m365-login-auth.php:931
+#: includes/class-m365-login-auth.php:1030
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:932
+#: includes/class-m365-login-auth.php:1031
msgid "The Microsoft sign-in could not be verified."
msgstr ""
-#: includes/class-m365-login-auth.php:933
+#: includes/class-m365-login-auth.php:1032
msgid "Your Microsoft account did not provide an e-mail address."
msgstr ""
-#: includes/class-m365-login-auth.php:934
+#: includes/class-m365-login-auth.php:1033
msgid "Your e-mail domain is not allowed to sign in here."
msgstr ""
-#: includes/class-m365-login-auth.php:935
+#: includes/class-m365-login-auth.php:1034
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr ""
-#: includes/class-m365-login-auth.php:936
+#: includes/class-m365-login-auth.php:1035
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:937
+#: includes/class-m365-login-auth.php:1036
msgid "You are not allowed to sign in with this account."
msgstr ""
-#: includes/class-m365-login-auth.php:938
+#: includes/class-m365-login-auth.php:1037
msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
msgstr ""
-#: includes/class-m365-login-auth.php:939
+#: includes/class-m365-login-auth.php:1038
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:940
+#: includes/class-m365-login-auth.php:1039
msgid "Your Microsoft account is a member of a group that is not allowed to sign in here."
msgstr ""
-#: includes/class-m365-login-auth.php:941
+#: includes/class-m365-login-auth.php:1040
msgid "The fallback key is not valid."
msgstr ""
-#: includes/class-m365-login-auth.php:942
+#: includes/class-m365-login-auth.php:1041
msgid "Too many attempts. Please wait 15 minutes."
msgstr ""
-#: includes/class-m365-login-auth.php:943
+#: includes/class-m365-login-auth.php:1042
msgid "Too many sign-in attempts from your connection. Please wait a few minutes and try again."
msgstr ""
-#: includes/class-m365-login-auth.php:944 includes/class-m365-login-sync.php:1520
+#: includes/class-m365-login-auth.php:1043 includes/class-m365-login-sync.php:1684
msgid "This account has been deactivated."
msgstr ""
+#: includes/class-m365-login-auth.php:1044
+msgid "For security reasons this administrator account can only be linked to a Microsoft account whose user principal name equals the WordPress e-mail address. Please contact an administrator."
+msgstr ""
+
+#: includes/class-m365-login-auth.php:1045
+msgid "Guest and external accounts cannot sign in here."
+msgstr ""
+
#: includes/class-m365-login-certificate.php:29
msgid "The PHP OpenSSL extension is not available."
msgstr ""
@@ -1181,30 +1209,38 @@ msgstr ""
msgid "The RSA key must have at least 2048 bits."
msgstr ""
-#: includes/class-m365-login-certificate.php:102
+#: includes/class-m365-login-certificate.php:101
+msgid "The certificate field contains a private key. Paste only the certificate (-----BEGIN CERTIFICATE-----) there."
+msgstr ""
+
+#: includes/class-m365-login-certificate.php:105 includes/class-m365-login-certificate.php:119
msgid "The certificate could not be read. Paste it in PEM format (-----BEGIN CERTIFICATE-----)."
msgstr ""
-#: includes/class-m365-login-certificate.php:105
+#: includes/class-m365-login-certificate.php:108
msgid "The certificate does not belong to this private key."
msgstr ""
-#: includes/class-m365-login-certificate.php:110
+#: includes/class-m365-login-certificate.php:113
msgid "The certificate has already expired."
msgstr ""
-#: includes/class-m365-login-graph.php:434
+#: includes/class-m365-login-graph.php:436
msgid "Group"
msgstr ""
-#: includes/class-m365-login-graph.php:436
+#: includes/class-m365-login-graph.php:438
msgid "Security group"
msgstr ""
-#: includes/class-m365-login-graph.php:438
+#: includes/class-m365-login-graph.php:440
msgid "Microsoft 365 group"
msgstr ""
+#: includes/class-m365-login-graph.php:444
+msgid "Public Microsoft 365 group – anyone in the organisation can join"
+msgstr ""
+
#: includes/class-m365-login-settings.php:55
msgid "Sign in with Microsoft"
msgstr ""
@@ -1213,352 +1249,370 @@ msgstr ""
msgid "or"
msgstr ""
-#: includes/class-m365-login-settings.php:226 includes/class-m365-login-settings.php:540
+#: includes/class-m365-login-settings.php:226 includes/class-m365-login-settings.php:543
msgid "The private key could not be encrypted. Is the OpenSSL extension available?"
msgstr ""
-#: includes/class-m365-login-settings.php:486
+#: includes/class-m365-login-settings.php:489
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr ""
-#: includes/class-m365-login-settings.php:494
+#: includes/class-m365-login-settings.php:497
msgid "The application (client) ID must be a GUID."
msgstr ""
-#: includes/class-m365-login-settings.php:506
+#: includes/class-m365-login-settings.php:509
msgid "The client secret contains invalid characters."
msgstr ""
-#: includes/class-m365-login-settings.php:510
+#: includes/class-m365-login-settings.php:513
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr ""
-#: includes/class-m365-login-settings.php:530
+#: includes/class-m365-login-settings.php:533
msgid "Please paste both the private key and the certificate."
msgstr ""
-#: includes/class-m365-login-settings.php:532
+#: includes/class-m365-login-settings.php:535
msgid "The pasted key or certificate is too large."
msgstr ""
-#: includes/class-m365-login-settings.php:549
+#: includes/class-m365-login-settings.php:552
msgid "Certificate authentication is selected but no certificate is stored yet. Generate one or paste your own; the Microsoft button stays hidden until then."
msgstr ""
-#: includes/class-m365-login-settings.php:582
+#: includes/class-m365-login-settings.php:585
msgid "The custom login page must be a URL on this site."
msgstr ""
-#: includes/class-m365-login-settings.php:689
+#: includes/class-m365-login-settings.php:692
msgid "User sync: \"Delete\" needs a user who receives the posts of deleted accounts. Until one is selected, accounts are deactivated instead."
msgstr ""
-#: includes/class-m365-login-sync.php:110
+#: includes/class-m365-login-sync.php:119
msgid "Display name"
msgstr ""
-#: includes/class-m365-login-sync.php:114
+#: includes/class-m365-login-sync.php:123
msgid "First name"
msgstr ""
-#: includes/class-m365-login-sync.php:118
+#: includes/class-m365-login-sync.php:127
msgid "Last name"
msgstr ""
-#: includes/class-m365-login-sync.php:122
+#: includes/class-m365-login-sync.php:131
msgid "Profile photo (used as avatar)"
msgstr ""
-#: includes/class-m365-login-sync.php:126
+#: includes/class-m365-login-sync.php:135
msgid "Job title"
msgstr ""
-#: includes/class-m365-login-sync.php:130
+#: includes/class-m365-login-sync.php:139
msgid "Department"
msgstr ""
-#: includes/class-m365-login-sync.php:134
+#: includes/class-m365-login-sync.php:143
msgid "Company"
msgstr ""
-#: includes/class-m365-login-sync.php:138
+#: includes/class-m365-login-sync.php:147
msgid "Office"
msgstr ""
-#: includes/class-m365-login-sync.php:142
+#: includes/class-m365-login-sync.php:151
msgid "Employee ID"
msgstr ""
-#: includes/class-m365-login-sync.php:146
+#: includes/class-m365-login-sync.php:155
msgid "Business phone"
msgstr ""
-#: includes/class-m365-login-sync.php:150
+#: includes/class-m365-login-sync.php:159
msgid "Mobile phone"
msgstr ""
-#: includes/class-m365-login-sync.php:154
+#: includes/class-m365-login-sync.php:163
msgid "Street address"
msgstr ""
-#: includes/class-m365-login-sync.php:158
+#: includes/class-m365-login-sync.php:167
msgid "Postal code"
msgstr ""
-#: includes/class-m365-login-sync.php:162
+#: includes/class-m365-login-sync.php:171
msgid "City"
msgstr ""
-#: includes/class-m365-login-sync.php:166
+#: includes/class-m365-login-sync.php:175
msgid "State / province"
msgstr ""
-#: includes/class-m365-login-sync.php:170
+#: includes/class-m365-login-sync.php:179
msgid "Country"
msgstr ""
-#: includes/class-m365-login-sync.php:174
+#: includes/class-m365-login-sync.php:183
msgid "Language (sets the admin language if installed)"
msgstr ""
-#: includes/class-m365-login-sync.php:309
+#: includes/class-m365-login-sync.php:319
msgid "Another sync is still running. Please try again in a few minutes."
msgstr ""
-#: includes/class-m365-login-sync.php:366
+#: includes/class-m365-login-sync.php:405
msgid "The connection to Microsoft Entra ID is not configured yet."
msgstr ""
-#: includes/class-m365-login-sync.php:370
+#: includes/class-m365-login-sync.php:409
msgid "The user sync needs a pinned tenant ID (GUID) on the Connection tab."
msgstr ""
-#: includes/class-m365-login-sync.php:374
+#: includes/class-m365-login-sync.php:413
msgid "The default role does not exist. Please check the sync settings."
msgstr ""
#. translators: %d: number of users
-#: includes/class-m365-login-sync.php:386
+#: includes/class-m365-login-sync.php:425
msgid "%d user read from Microsoft 365."
msgid_plural "%d users read from Microsoft 365."
msgstr[0] ""
msgstr[1] ""
+#: includes/class-m365-login-sync.php:436
+msgid "Microsoft 365 returned no users at all while accounts are linked. Nothing was changed. Check the tenant and the sync groups."
+msgstr ""
+
+#: includes/class-m365-login-sync.php:484
+msgid "The tenant ID changed since the last sync. Accounts linked in the old tenant are not found in the new one, so no account was deactivated or deleted in this run. Run the sync again to apply deactivations."
+msgstr ""
+
#. translators: 1: number of accounts, 2: limit
-#: includes/class-m365-login-sync.php:443
+#: includes/class-m365-login-sync.php:496
msgid "Safety stop: %1$d accounts would be deactivated or deleted, more than the limit of %2$d per run. No account was deactivated or deleted. Check the sync groups and the tenant, then run the sync again (the limit can be changed with the m365_login_sync_deprovision_limit filter)."
msgstr ""
+#: includes/class-m365-login-sync.php:621 includes/class-m365-login-sync.php:674 includes/class-m365-login-sync.php:1028 includes/class-m365-login-sync.php:1816
+msgid "disabled in Microsoft 365"
+msgstr ""
+
#. translators: %s: user principal name
-#: includes/class-m365-login-sync.php:567
+#: includes/class-m365-login-sync.php:632
msgid "%s: no usable e-mail address, skipped."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:574
+#: includes/class-m365-login-sync.php:639
msgid "%s: e-mail domain is not on the allow-list, skipped."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:586
+#: includes/class-m365-login-sync.php:651
msgid "%s: the WordPress account with this e-mail address is linked to a different Microsoft account, skipped."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:590
-msgid "%s: existing account linked."
-msgstr ""
-
-#: includes/class-m365-login-sync.php:604 includes/class-m365-login-sync.php:955 includes/class-m365-login-sync.php:1652
-msgid "disabled in Microsoft 365"
+#: includes/class-m365-login-sync.php:656
+msgid "%s: privileged WordPress account – it is only linked when the Microsoft user principal name equals its e-mail address (member account, no guest). Skipped."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:615
+#: includes/class-m365-login-sync.php:660
+msgid "%s: existing account linked."
+msgstr ""
+
+#. translators: %s: e-mail address
+#: includes/class-m365-login-sync.php:685
msgid "%s: added to this site."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:624
+#: includes/class-m365-login-sync.php:694
msgid "%s: reactivated (active in Microsoft 365 again)."
msgstr ""
#. translators: 1: e-mail address, 2: list of changed fields
-#: includes/class-m365-login-sync.php:635
+#: includes/class-m365-login-sync.php:705
msgid "%1$s: updated (%2$s)."
msgstr ""
#. translators: 1: e-mail address, 2: role names
-#: includes/class-m365-login-sync.php:661
+#: includes/class-m365-login-sync.php:731
msgid "%1$s: account created (%2$s)."
msgstr ""
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:688
+#: includes/class-m365-login-sync.php:758
msgid "%1$s: account could not be created: %2$s"
msgstr ""
+#. translators: 1: current e-mail address, 2: e-mail address in Microsoft 365
+#: includes/class-m365-login-sync.php:816
+msgid "%1$s: the e-mail address in Microsoft 365 changed to %2$s. It is not changed automatically for privileged accounts – update it by hand if intended."
+msgstr ""
+
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:746
+#: includes/class-m365-login-sync.php:819
msgid "%s: e-mail address is used by another WordPress account and was not changed."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:749
+#: includes/class-m365-login-sync.php:822
msgid "e-mail"
msgstr ""
#. translators: %s: profile field
-#: includes/class-m365-login-sync.php:799
+#: includes/class-m365-login-sync.php:872
msgid "%s removed"
msgstr ""
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:809
+#: includes/class-m365-login-sync.php:882
msgid "%1$s: profile could not be updated: %2$s"
msgstr ""
#. translators: %s: role names
-#: includes/class-m365-login-sync.php:915
+#: includes/class-m365-login-sync.php:988
msgid "roles: %s"
msgstr ""
-#: includes/class-m365-login-sync.php:950 includes/class-m365-login-sync.php:1653
+#: includes/class-m365-login-sync.php:1023 includes/class-m365-login-sync.php:1817
msgid "deleted in Microsoft 365"
msgstr ""
-#: includes/class-m365-login-sync.php:958 includes/class-m365-login-sync.php:1654
+#: includes/class-m365-login-sync.php:1031 includes/class-m365-login-sync.php:1818
msgid "no longer a member of the sync groups"
msgstr ""
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:978
+#: includes/class-m365-login-sync.php:1063
msgid "%1$s: %2$s, but the account is protected (administrator or your own account) and was not changed."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1011
+#: includes/class-m365-login-sync.php:1140
msgid "%s: no valid user to receive the content is selected, so the account is deactivated instead of deleted."
msgstr ""
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:1020
+#: includes/class-m365-login-sync.php:1149
msgid "%1$s: account deleted (%2$s)."
msgstr ""
#. translators: 1: e-mail address, 2: reason
-#: includes/class-m365-login-sync.php:1029
+#: includes/class-m365-login-sync.php:1158
msgid "%1$s: account deactivated (%2$s)."
msgstr ""
-#: includes/class-m365-login-sync.php:1108
+#: includes/class-m365-login-sync.php:1237
msgid "Microsoft Graph refused the request. Grant the application permissions \"User.Read.All\" and \"GroupMember.Read.All\" with admin consent in Entra ID."
msgstr ""
#. translators: %s: error message
-#: includes/class-m365-login-sync.php:1111
+#: includes/class-m365-login-sync.php:1240
msgid "Microsoft Graph error: %s"
msgstr ""
-#: includes/class-m365-login-sync.php:1129
+#: includes/class-m365-login-sync.php:1258
msgid "Log truncated."
msgstr ""
#. translators: 1: e-mail address, 2: error message
-#: includes/class-m365-login-sync.php:1205
+#: includes/class-m365-login-sync.php:1334
msgid "%1$s: profile photo could not be read: %2$s"
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1229 includes/class-m365-login-sync.php:1272
+#: includes/class-m365-login-sync.php:1358 includes/class-m365-login-sync.php:1401
msgid "%s: profile photo updated."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1248
+#: includes/class-m365-login-sync.php:1377
msgid "%s: profile photo could not be downloaded."
msgstr ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1255
+#: includes/class-m365-login-sync.php:1384
msgid "%s: profile photo is not a valid image or could not be saved."
msgstr ""
#. translators: %d: number of photos
-#: includes/class-m365-login-sync.php:1278
+#: includes/class-m365-login-sync.php:1407
msgid "%d changed profile photo will be downloaded in the next run (download limit per run reached)."
msgid_plural "%d changed profile photos will be downloaded in the next run (download limit per run reached)."
msgstr[0] ""
msgstr[1] ""
#. translators: %s: e-mail address
-#: includes/class-m365-login-sync.php:1293
+#: includes/class-m365-login-sync.php:1422
msgid "%s: profile photo removed."
msgstr ""
#. translators: %d: number of photos
-#: includes/class-m365-login-sync.php:1321
+#: includes/class-m365-login-sync.php:1450
msgid "Profile photo sync is off: %d stored photo removed."
msgid_plural "Profile photo sync is off: %d stored photos removed."
msgstr[0] ""
msgstr[1] ""
-#: includes/class-m365-login-sync.php:1549 includes/class-m365-login-sync.php:1682
+#: includes/class-m365-login-sync.php:1713 includes/class-m365-login-sync.php:1846
msgid "Microsoft 365"
msgstr ""
-#: includes/class-m365-login-sync.php:1567
+#: includes/class-m365-login-sync.php:1731
msgid "Deactivated"
msgstr ""
-#: includes/class-m365-login-sync.php:1570
+#: includes/class-m365-login-sync.php:1734
msgid "Imported"
msgstr ""
-#: includes/class-m365-login-sync.php:1572
+#: includes/class-m365-login-sync.php:1736
msgid "Linked"
msgstr ""
-#: includes/class-m365-login-sync.php:1600
+#: includes/class-m365-login-sync.php:1764
msgid "Reactivate"
msgstr ""
-#: includes/class-m365-login-sync.php:1600
+#: includes/class-m365-login-sync.php:1764
msgid "Deactivate"
msgstr ""
-#: includes/class-m365-login-sync.php:1633
+#: includes/class-m365-login-sync.php:1797
msgid "The account has been deactivated and signed out everywhere."
msgstr ""
-#: includes/class-m365-login-sync.php:1634
+#: includes/class-m365-login-sync.php:1798
msgid "The account has been reactivated."
msgstr ""
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1658
+#: includes/class-m365-login-sync.php:1822
msgid "Deactivated since %1$s (%2$s)"
msgstr ""
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1660
+#: includes/class-m365-login-sync.php:1824
msgid "manually"
msgstr ""
#. translators: 1: date, 2: reason
-#: includes/class-m365-login-sync.php:1662
+#: includes/class-m365-login-sync.php:1826
msgid "Status"
msgstr ""
-#: includes/class-m365-login-sync.php:1665
+#: includes/class-m365-login-sync.php:1829
msgid "Object ID"
msgstr ""
-#: includes/class-m365-login-sync.php:1669
+#: includes/class-m365-login-sync.php:1833
msgid "Last sync"
msgstr ""
-#: includes/class-m365-login-sync.php:1691
+#: includes/class-m365-login-sync.php:1855
msgid "These values are managed by the Microsoft 365 user sync and overwritten on the next run."
msgstr ""
diff --git a/readme.txt b/readme.txt
index 7decb55..5249018 100644
--- a/readme.txt
+++ b/readme.txt
@@ -34,7 +34,8 @@ The plugin is deliberately small and strict:
* Optional **tenant pinning**: when a tenant GUID is configured, tokens from any other tenant are rejected.
* **Account binding**: on first sign-in the immutable Microsoft object ID is stored with the user; later sign-ins with the same e-mail but a different Microsoft identity are refused.
* Optional **e-mail domain allow-list** and **group allow-list** (fails closed when membership cannot be verified).
-* **Button-only mode** blocks password sign-in server-side, not just visually; the fallback key is rate limited and never stored in a cookie.
+* **Button-only mode** blocks password sign-in server-side, not just visually – everywhere, XML-RPC and other plugins' login handlers included (application passwords and WP-CLI keep working, API requests never get a login cookie). Right and wrong passwords get the same answer. The fallback cookie carries its issue time and expires on the server.
+* Administrator accounts are only linked (by sign-in or sync) through a matching user principal name of a member account, never through the freely settable e-mail attribute; one Microsoft identity can only be bound to one WordPress account.
* **Certificate authentication** (RFC 7523 client assertion) as an alternative to a client secret: generate a 3072-bit key pair in the settings, upload only the public certificate to Entra ID. Nothing secret is ever transmitted.
* The **client secret / private key is encrypted at rest** (AES-256-GCM, key derived from your WordPress salts) and never displayed again.
* In multi-tenant mode the unverified `email` claim is ignored; matching uses the user principal name (verified domain) only.
@@ -132,11 +133,11 @@ Yes. Forms rendered with `wp_login_form()` get the button automatically. For pag
= Does it support multisite? =
-Yes. Settings are per site; a user must be a member of the site (or a super admin) to sign in.
+Yes. Settings are per site but can only be changed by super admins, because they decide which Microsoft identity may sign in as which network-wide WordPress user. A user must be a member of the site (or a super admin) to sign in.
= What happens on uninstall? =
-The settings, cached data, the sync report and schedule, stored profile photos and the per-user plugin data (Microsoft object ID, deactivation status) are removed. Imported accounts and copied profile fields (`m365_*` user meta) are kept. Deactivated accounts become active again, so delete them first if they must stay locked.
+The settings, cached data, the sync report and schedule, stored profile photos and the per-user plugin data (Microsoft object ID, deactivation status) are removed. Imported accounts and copied profile fields (`m365_*` user meta) are kept. Deactivated accounts stay without a role, with a random password and without application passwords.
== Screenshots ==
@@ -156,6 +157,7 @@ The settings, cached data, the sync report and schedule, stored profile photos a
* New: `wp m365-login sync [--dry-run]` WP-CLI command and scheduled sync via WP-Cron.
* New: excluded Entra groups – their members can never sign in with Microsoft.
* Fix: failed Microsoft sign-ins (e.g. expired secret, group not allowed) ended in a PHP fatal error instead of the error message.
+* Security: fixes from a full security audit – see docs/security-audit.md (button-only bypasses via XML-RPC/REST, administrator linking, multisite settings restricted to super admins, deactivation hardening, and more).
* Fix: generating or removing the certificate in the settings did not keep the change and broke a stored client secret.
= 1.0.0 =
diff --git a/uninstall.php b/uninstall.php
index f0ad67c..5c5c640 100644
--- a/uninstall.php
+++ b/uninstall.php
@@ -17,7 +17,18 @@ global $wpdb;
function m365_login_uninstall_site() {
global $wpdb;
+ // Cached Graph app token (may live in a persistent object cache instead of the options table).
+ $settings = get_option( 'm365_login_settings', array() );
+ if ( is_array( $settings ) && ! empty( $settings['client_id'] ) ) {
+ $tenant = ! empty( $settings['tenant_id'] ) ? $settings['tenant_id'] : 'organizations';
+ foreach ( array( 'secret', 'certificate' ) as $method ) {
+ delete_transient( 'm365_login_apptoken_' . md5( $tenant . '|' . $settings['client_id'] . '|' . $method ) );
+ }
+ }
+
delete_option( 'm365_login_settings' );
+ delete_option( 'm365_login_sync_lock' );
+ delete_option( 'm365_login_sync_tenant' );
delete_option( 'm365_login_sync_report' );
wp_clear_scheduled_hook( 'm365_login_sync' );