diff --git a/.github/assets/screenshots/settings-connection.png b/.github/assets/screenshots/settings-connection.png index c5d1fe2..ac0bd92 100644 Binary files a/.github/assets/screenshots/settings-connection.png and b/.github/assets/screenshots/settings-connection.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 00a7802..2151d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to this project are documented in this file. The format foll - Settings screen (Settings → M365 Login) with connection, button and security tabs, live button preview, colour presets, media-library icon picker, redirect-URI copy button and tenant connectivity test. - ID token verification against Microsoft's JWKS (RS256, issuer, audience, tenant, expiry, nonce). - Encrypted client secret storage (AES-256-GCM). +- Certificate based client authentication (RFC 7523 `private_key_jwt`): one-click generation of a 3072-bit RSA key pair with self-signed certificate, `.cer` download, own PEM upload, expiry display; step-by-step guides for both methods in the settings. +- Security audit (docs/security-audit.md) with fixes: unverified `email` claim ignored in multi-tenant mode, login-start rate limit, trusted proxy IP header. - Account binding to the Microsoft object ID, e-mail domain allow-list. - Entra group restriction with a Graph-powered group picker; membership verified via the `groups` claim or Microsoft Graph `checkMemberGroups`. - Button-only mode that hides the password form and blocks password sign-in on `wp-login.php`, with a secret, rate-limited fallback link and a `wp-config.php` emergency constant. diff --git a/README.md b/README.md index 8a03aca..7ada872 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ | 🎨 **Gestaltbarer Button** | Text, Icon (Microsoft-Logo oder eigenes Bild), Farben, Hover-Farbe, Rahmen, Eckenradius, Position – mit Live-Vorschau und Presets. | | 👥 **Entra-Gruppen** | Optional nur Mitglieder ausgewählter Gruppen zulassen. Gruppen werden direkt im Backend gesucht und ausgewählt. | | 🚪 **Nur-Button-Modus** | Passwortfelder ausblenden und Passwort-Logins sperren – mit geheimem Fallback-Link als Notausgang. | -| 🛡️ **Sicher by default** | OpenID Connect + PKCE, Signaturprüfung, Tenant-Pinning, Konto-Bindung, verschlüsseltes Secret. | +| 🔏 **Secret oder Zertifikat** | Wahlweise Client Secret oder zertifikatsbasierte Authentifizierung (RFC 7523). Zertifikat mit einem Klick im Backend erzeugen, nur der öffentliche Teil geht zu Microsoft. | +| 🛡️ **Sicher by default** | OpenID Connect + PKCE, Signaturprüfung, Tenant-Pinning, Konto-Bindung, verschlüsseltes Secret, [Security-Audit](docs/security-audit.md). | | 🌍 **Übersetzbar** | Englische Basis, deutsche Übersetzung (du & Sie) enthalten. | | 📦 **WordPress.org-ready** | readme.txt, Lizenz, Uninstall, Plugin Check in CI, Build-Script. | @@ -142,8 +143,9 @@ Alternativ den Repo-Inhalt als Ordner `m365-login` nach `wp-content/plugins/` ko - Unterstützte Kontotypen: **Nur Konten in diesem Organisationsverzeichnis** (Single Tenant) - Umleitungs-URI: Plattform **Web**, URI aus Schritt 1 3. **IDs übernehmen.** Auf der Übersichtsseite **Anwendungs-ID (Client)** und **Verzeichnis-ID (Mandant)** kopieren → in WordPress eintragen. -4. **Client Secret erstellen.** *Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel* → den **Wert** (nicht die Geheimnis-ID) in WordPress eintragen. - Das Ablaufdatum notieren – abgelaufene Secrets müssen erneuert werden. +4. **Authentifizierung wählen.** + - *Zertifikat (empfohlen):* In WordPress *Zertifikat erzeugen* → *.cer herunterladen* → in Entra ID *Zertifikate & Geheimnisse → Zertifikate → Zertifikat hochladen*. Thumbprint vergleichen. + - *Client Secret:* *Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel* → den **Wert** (nicht die Geheimnis-ID) in WordPress eintragen. Ablaufdatum notieren. 5. **E-Mail-Claim aktivieren** (empfohlen). *Tokenkonfiguration → Optionalen Anspruch hinzufügen → ID → `email`*. 6. **Speichern** und mit *Tenant testen* prüfen, ob Microsoft erreichbar ist. @@ -181,9 +183,11 @@ Wichtig: Jeder Benutzer, der sich per Microsoft anmelden soll, braucht in WordPr | Feld | Beschreibung | | --- | --- | -| Verzeichnis-ID (Tenant) | GUID des Tenants (empfohlen, aktiviert Tenant-Pinning) oder `organizations` / `common` / `consumers`. | +| Verzeichnis-ID (Tenant) | GUID des Tenants (empfohlen, aktiviert Tenant-Pinning) oder `organizations` / `common` / `consumers`. Im Multi-Tenant-Modus wird nur der UPN zur Zuordnung verwendet (siehe Audit H-1). | | Anwendungs-ID (Client) | GUID der App-Registrierung. | +| Authentifizierung | **Client Secret** oder **Zertifikat** (empfohlen). Für beide Wege gibt es im Backend eine Schritt-für-Schritt-Anleitung. | | Client Secret | Wird verschlüsselt gespeichert und nie wieder angezeigt. Leer lassen = behalten. | +| Zertifikat | *Zertifikat erzeugen* legt ein 3072-Bit-RSA-Schlüsselpaar mit selbstsigniertem Zertifikat (2 Jahre) an. Der private Schlüssel bleibt verschlüsselt auf dem Server; die `.cer`-Datei wird in Entra ID unter *Zertifikate & Geheimnisse → Zertifikate* hochgeladen. Alternativ eigenes PEM-Paar einfügen. | | Kontoauswahl | `select_account` (Standard), `none` (bestehende Microsoft-Sitzung nutzen) oder `login` (immer Anmeldedaten verlangen). | | Tenant testen | Lädt die OpenID-Konfiguration des Tenants – prüft ID und ausgehende Verbindung. | @@ -282,12 +286,16 @@ Plugins, die `wp-login.php` umbenennen (z. B. WPS Hide Login), sind kompatibel, | Fremde Tenants | Bei konfigurierter Tenant-GUID **Tenant-Pinning**; sonst Issuer-Konsistenz mit `tid`. | | Kontoübernahme per E-Mail-Recycling | **Bindung an die Objekt-ID** (`oid`) beim ersten Login. | | Unbefugte Konten | Kein Provisioning, optionale Domain-Allowlist, optionale Gruppen-Beschränkung (fail closed). | -| Secret-Diebstahl aus der Datenbank | AES-256-GCM, Schlüssel per HKDF aus `AUTH_KEY`/`SECURE_AUTH_KEY`; ohne `wp-config.php` ist der Datensatz wertlos. | +| Secret-Diebstahl aus der Datenbank | AES-256-GCM, Schlüssel per HKDF aus `AUTH_KEY`/`SECURE_AUTH_KEY`; ohne `wp-config.php` ist der Datensatz wertlos. Gilt für Client Secret und privaten Zertifikatsschlüssel. | +| Secret-Abfluss im Transport | Zertifikatsmodus: es wird nie ein Geheimnis übertragen, nur eine 5 Minuten gültige, signierte Client Assertion (RFC 7523). | +| Kontoübernahme im Multi-Tenant-Modus | `email`-Claim fremder Tenants wird ignoriert (nur UPN mit verifizierter Domain oder `xms_edov`). | +| Flooding der State-Tabelle | Max. 30 Login-Starts pro IP und 10 Minuten; Proxy-Header per `M365_LOGIN_CLIENT_IP_HEADER`. | | Offene Redirects | `redirect_to` läuft durch `wp_validate_redirect`, alle Redirects über `wp_safe_redirect`. | | Fehler-Reflektion | Fehlermeldungen sind Codes → feste, übersetzte Texte; Details nur ins Log (`WP_DEBUG_LOG`). | | Rate Limiting Fallback-Key | 10 Fehlversuche pro IP / 15 Min. | -Die Klassen für JWT-Prüfung und Verschlüsselung haben isolierte Tests (manipulierte Signaturen, abgelaufene Tokens, falsche Audience/Tenant/Issuer, `alg=none`, fremde Schlüssel). +Die Klassen für JWT-Prüfung, Verschlüsselung, Zertifikate und die Login-Sperre haben isolierte Tests (manipulierte Signaturen, abgelaufene Tokens, falsche Audience/Tenant/Issuer, `alg=none`, fremde Schlüssel, gefälschte Fallback-Cookies, schwache RSA-Schlüssel). +Der vollständige Bericht mit Bedrohungsmodell, Befunden und Betriebsempfehlungen: **[docs/security-audit.md](docs/security-audit.md)**. --- diff --git a/assets/css/admin.css b/assets/css/admin.css index 40756cc..c627454 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -718,3 +718,230 @@ .m365-fallback .m365-copy { margin-bottom: 8px; } + +/* Auth method switch */ +.m365-method { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 20px; +} + +@media (max-width: 782px) { + .m365-method { + grid-template-columns: 1fr; + } +} + +.m365-method__option { + display: flex; + gap: 10px; + align-items: flex-start; + padding: 14px 16px; + border: 1px solid var(--m365-border); + border-radius: 8px; + cursor: pointer; + background: #fff; +} + +.m365-method__option:hover { + border-color: #b6d7f2; +} + +.m365-method__option.is-selected { + border-color: var(--m365-accent); + box-shadow: 0 0 0 1px var(--m365-accent); + background: #fbfdff; +} + +.m365-method__option input { + margin-top: 3px; +} + +.m365-method__option strong { + display: block; + font-weight: 600; +} + +.m365-method__option em { + display: block; + font-style: normal; + color: var(--m365-muted); + margin-top: 2px; + font-size: 12px; +} + +.m365-method__badge { + display: inline-block; + margin-left: 6px; + padding: 1px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + background: #e6f4ea; + color: #1e6b31; + vertical-align: middle; +} + +.m365-auth-panel { + display: none; +} + +.m365-auth-panel.is-active { + display: block; +} + +/* Certificate box */ +.m365-cert { + border: 1px solid var(--m365-border); + border-radius: 8px; + padding: 16px; + background: #fafafa; + margin-bottom: 16px; +} + +.m365-cert__grid { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: 6px 16px; + font-size: 13px; + margin: 8px 0 12px; +} + +.m365-cert__grid dt { + color: var(--m365-muted); +} + +.m365-cert__grid dd { + margin: 0; + word-break: break-all; +} + +.m365-cert__grid code { + background: #fff; + border: 1px solid var(--m365-border); + border-radius: 4px; + padding: 2px 6px; + font-size: 12px; +} + +.m365-cert__actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.m365-cert__empty { + color: var(--m365-muted); + margin: 0 0 12px; +} + +.m365-cert__status { + display: inline-block; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; +} + +.m365-cert__status.is-ok { + background: #e6f4ea; + color: #1e6b31; +} + +.m365-cert__status.is-warn { + background: #fcf0e4; + color: #8a4b00; +} + +.m365-cert__status.is-bad { + background: #fcf0f1; + color: #8a2424; +} + +.m365-field textarea.m365-pem { + font-family: Consolas, Monaco, monospace; + font-size: 12px; + min-height: 120px; +} + +/* Guides */ +.m365-guide { + border: 1px solid var(--m365-border); + border-radius: 8px; + background: #fff; + margin-top: 16px; +} + +.m365-guide > summary { + cursor: pointer; + padding: 12px 16px; + font-weight: 600; + list-style: none; + display: flex; + align-items: center; + gap: 8px; +} + +.m365-guide > summary::-webkit-details-marker { + display: none; +} + +.m365-guide > summary::before { + content: "▸"; + color: var(--m365-accent); + transition: transform 0.15s ease; +} + +.m365-guide[open] > summary::before { + transform: rotate(90deg); +} + +.m365-guide__body { + padding: 0 16px 16px; + border-top: 1px solid #f0f0f1; +} + +.m365-guide__body ol { + margin: 12px 0 0; + padding-left: 22px; +} + +.m365-guide__body li { + margin-bottom: 10px; + line-height: 1.55; +} + +.m365-guide__body li strong { + font-weight: 600; +} + +.m365-guide__body code { + font-size: 12px; +} + +.m365-guide__note { + margin: 12px 0 0; + padding: 10px 12px; + border-radius: 6px; + background: #f0f6fc; + border: 1px solid #c5d9ed; + font-size: 13px; +} + +.m365-guide__warn { + margin: 12px 0 0; + padding: 10px 12px; + border-radius: 6px; + background: #fcf0e4; + border: 1px solid #f0c28a; + color: #6b3a00; + font-size: 13px; +} + +.m365-warning--strong { + background: #fcf0f1; + color: #8a2424; + border: 1px solid #f0b8bd; +} diff --git a/assets/js/admin.js b/assets/js/admin.js index ccba121..a977db2 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -129,6 +129,50 @@ $( '#m365-icon-url' ).val( '' ).trigger( 'input' ); } ); + /* ---------------- Auth method switch ---------------- */ + function applyMethod() { + var method = $( 'input[name$="[auth_method]"]:checked' ).val() || 'secret'; + $( '.m365-method__option' ).removeClass( 'is-selected' ).filter( '[data-method="' + method + '"]' ).addClass( 'is-selected' ); + $( '.m365-auth-panel' ).removeClass( 'is-active' ).filter( '[data-method="' + method + '"]' ).addClass( 'is-active' ); + } + $( 'input[name$="[auth_method]"]' ).on( 'change', applyMethod ); + applyMethod(); + + /* ---------------- Certificate generation ---------------- */ + $( '#m365-cert-generate' ).on( 'click', function () { + var $btn = $( this ); + var $out = $( '#m365-cert-result' ); + if ( $btn.data( 'replace' ) && ! window.confirm( i18n.confirmCert ) ) { + return; + } + $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 ) { + if ( res && res.success ) { + $out.addClass( 'is-success' ).text( res.data.message ); + window.setTimeout( function () { window.location.reload(); }, 800 ); + } else { + $out.addClass( 'is-error' ).text( ( res && res.data && res.data.message ) || i18n.testFailed ); + $btn.prop( 'disabled', false ); + } + } ).fail( function () { + $out.addClass( 'is-error' ).text( i18n.testFailed ); + $btn.prop( 'disabled', false ); + } ); + } ); + + $( '#m365-cert-remove' ).on( 'change', function () { + if ( this.checked && ! window.confirm( i18n.confirmCertRemove ) ) { + this.checked = false; + } + } ); + + $( '#m365-cert-paste-toggle' ).on( 'click', function ( e ) { + e.preventDefault(); + $( '#m365-cert-paste' ).prop( 'hidden', function ( i, v ) { return ! v; } ); + } ); + /* ---------------- Secret visibility ---------------- */ $( '.m365-toggle-secret' ).on( 'click', function () { var $input = $( '#m365-client-secret' ); diff --git a/docs/security-audit.md b/docs/security-audit.md new file mode 100644 index 0000000..2d723e0 --- /dev/null +++ b/docs/security-audit.md @@ -0,0 +1,149 @@ +# Security-Audit: M365 Login 1.0.0 + +**Stand:** 22.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, +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. + +> 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. + +## 1. Zusammenfassung + +| Schweregrad | Gefunden | Behoben | Offen (mit Empfehlung) | +| --- | --- | --- | --- | +| Hoch | 1 | 1 | 0 | +| Mittel | 3 | 3 | 0 | +| Niedrig | 5 | 3 | 2 | +| Hinweis | 6 | – | 6 | + +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. + +## 2. Bedrohungsmodell + +**Schutzziele:** (1) Nur die Person, die ein Microsoft-Konto kontrolliert, darf sich als der zugehörige +WordPress-Benutzer anmelden. (2) Client Secret bzw. privater Schlüssel dürfen nicht abfließen. (3) Der +Nur-Button-Modus darf nicht umgangen werden. (4) Keine Rechteausweitung über die Admin-Oberfläche. + +**Angreifer:** (A) anonymer Internet-Nutzer, (B) Benutzer eines fremden Entra-Tenants, (C) Benutzer des eigenen +Tenants ohne WordPress-Konto, (D) Angreifer mit Lesezugriff auf die Datenbank (Backup, SQL-Injection in anderem +Plugin), (E) Angreifer im Netzwerkpfad (MITM), (F) angemeldeter WordPress-Benutzer mit niedriger Rolle. + +## 3. Befunde + +### H-1 · Multi-Tenant-Modus: Kontoübernahme über den `email`-Claim — **behoben** + +**Beschreibung.** Bei Tenant `organizations`/`common` akzeptiert das Plugin Tokens beliebiger Tenants. Der +`email`-Claim in Entra-ID-Tokens ist ein frei editierbares Benutzerattribut des ausstellenden Tenants. Angreifer (B) +legt in seinem eigenen Tenant einen Benutzer mit `mail = admin@opfer.de` an und meldet sich damit an; das Plugin +findet den WordPress-Admin per E-Mail. Mit gepinnter Tenant-GUID (Standard-Empfehlung) war der Angriff nicht möglich. + +**Fix.** `M365_Login_Auth::email_from_claims()`: Im Multi-Tenant-Modus wird ausschließlich der UPN +(`preferred_username`, dessen Domain im ausstellenden Tenant verifiziert sein muss) verwendet; der `email`-Claim +nur, wenn Microsoft ihn per `xms_edov = true` als domain-verifiziert markiert. Zusätzlich deutlicher Warnhinweis im +Backend und Empfehlung der Domain-Allowlist. Test: `multi-tenant: unverified email claim ignored`. + +**Restrisiko.** Ein fremder Tenant kann eine Domain nur verifizieren, wenn er sie kontrolliert. Für maximale +Sicherheit bleibt die Tenant-GUID die Empfehlung. + +### M-1 · Passwort-Login-Sperre nur auf `wp-login.php` — **behoben** + +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 +Opt-out-Filter für vertrauenswürdige Plugins. Tests: `password login blocked without fallback cookie`, +`forged fallback cookie rejected`, `REST requests exempt`. + +### M-2 · `authenticate`-Filter mit zu früher Priorität — **behoben** + +Erste Fassung hing bei Priorität 5; `wp_authenticate_username_password()` (Priorität 20) überschreibt einen +übergebenen `WP_Error`, wenn Benutzername und Passwort stimmen — die Sperre wäre wirkungslos gewesen. Korrigiert auf +Priorität 99 und Blockade nur, wenn bereits ein `WP_User` vorliegt (Core-Fehlermeldungen bleiben erhalten). + +### M-3 · Unbegrenzte Erzeugung von State-Datensätzen — **behoben** + +Jeder Aufruf von `wp-login.php?action=m365_login` legt einen Transient (10 Min.) an. Angreifer (A) konnte die +Options-Tabelle fluten. Jetzt max. 30 Starts pro Client-IP und 10 Minuten (`too_many_attempts`). + +### N-1 · Rate-Limit nach `REMOTE_ADDR` hinter Reverse Proxy — **behoben (opt-in)** + +Hinter Cloudflare/Load Balancer teilen sich alle Besucher eine IP: Fehlversuche eines Angreifers sperren alle +(DoS auf den Fallback-Link), bzw. der Angreifer verteilt sich nicht. Neu: Konstante `M365_LOGIN_CLIENT_IP_HEADER` +bzw. Filter `m365_login_client_ip_header` zur Angabe eines vertrauenswürdigen Proxy-Headers. Nicht automatisch +aktiv, weil ein Client-Header ohne Proxy fälschbar wäre. Der Fallback-Key selbst hat ≈139 Bit Entropie; das +Rate-Limit ist Defense-in-Depth, kein primärer Schutz. + +### N-2 · Client Secret / privater Schlüssel: Schlüsselableitung aus den WordPress-Salts — **behoben (Design)** + +AES-256-GCM mit HKDF aus `AUTH_KEY`/`SECURE_AUTH_KEY`. Angreifer (D) mit reinem DB-Zugriff kann die Werte nicht +entschlüsseln. Angreifer mit Zugriff auf `wp-config.php` hat ohnehin Vollzugriff. Bewertung: angemessen für die +Plattform; ein HSM/KMS ist in WordPress nicht praktikabel. **Hinweis:** Rotation der Salts macht gespeicherte Werte +unlesbar (Plugin meldet „nicht konfiguriert“, Neueingabe nötig) — im Backend dokumentiert. + +### N-3 · Zertifikats-Authentifizierung (RFC 7523) — **neu, geprüft** + +Client Assertion: `RS256`, `aud` = Token-Endpunkt, `iss`/`sub` = Client-ID, `jti` 192 Bit Zufall, `exp` 5 Min., +Header `x5t` und `x5t#S256`. Privater Schlüssel: 3072 Bit RSA, verschlüsselt gespeichert, nie ausgegeben (Download +liefert ausschließlich das Zertifikat, Capability + Nonce geprüft). Eigene PEM-Paare: Prüfung auf RSA ≥ 2048 Bit, +Schlüssel-Zertifikat-Zugehörigkeit, Ablaufdatum; verschlüsselte Keys werden abgelehnt (keine Passphrase-Speicherung). +Tests: `assertion signature verifies with certificate`, `mismatched key/cert rejected`, `1024-bit key rejected`. + +### N-4 · Fehlermeldungen als Codes — **in Ordnung** + +`m365_error` transportiert nur einen Whitelist-Code; Texte sind fest und übersetzt. Keine Reflektion von +Microsoft-Fehlertexten an Endnutzer (nur ins Debug-Log). Kein Unterschied zwischen „Benutzer existiert nicht“ und +anderen Fehlern gegenüber Angreifer (C)? — Doch: `no_user` ist unterscheidbar. Bewertung: akzeptabel, weil der +Angreifer dafür bereits ein gültiges Konto des Tenants braucht und die Information (welche E-Mails ein WP-Konto +haben) für Tenant-Mitglieder unkritisch ist. `wp_login_failed` wird ausgelöst, damit Limit-Login-Plugins zählen. + +### N-5 · Transient-Verlust bei Object-Cache-Eviction — **offen, Hinweis** + +State-Datensätze liegen in Transients. Bei einem Object Cache mit aggressiver Eviction (kleiner Redis) kann ein +State vor Ablauf verschwinden → „Login request expired“. Kein Sicherheitsproblem (fail closed), aber ein +Verfügbarkeitsthema. Empfehlung: ausreichende Cache-Größe oder `m365_login_*`-Keys von der Eviction ausnehmen. + +### N-6 · `https_ssl_verify` durch Dritte deaktivierbar — **offen, Hinweis** + +Alle Requests laufen über die WordPress-HTTP-API mit Zertifikatsprüfung. Setzt ein anderes Plugin +`add_filter('https_ssl_verify','__return_false')`, wäre Angreifer (E) in der Lage, JWKS und Token-Endpunkt zu +fälschen. Das Plugin erzwingt `sslverify => true` für seine eigenen Requests nicht explizit, weil WordPress-Konventionen +den Site-Betreiber entscheiden lassen. Empfehlung: `https_ssl_verify` nie global deaktivieren. + +## 4. Geprüfte Kontrollen (ohne Befund) + +| Bereich | Kontrolle | Ergebnis | +| --- | --- | --- | +| Autorisierungsanfrage | `state` 256 Bit, `nonce` 256 Bit, PKCE-Verifier 512 Bit (S256), `response_mode=query` | ✔ | +| State-Bindung | HMAC-Schlüssel in DB, Klartext nur in URL; HttpOnly/SameSite=Lax/Secure-Cookie mit separatem Token, Hash im Datensatz; einmalige Einlösung (Delete vor Prüfung); TTL 10 Min. | ✔ Login-CSRF und Replay ausgeschlossen | +| Token-Austausch | Server-zu-Server, Secret/Assertion nie im Browser; `redirect_uri` fest aus `home_url()` | ✔ | +| ID-Token | Nur `RS256`; `alg=none`/HMAC abgelehnt; `kid` Pflicht; JWKS über HTTPS, Cache 12 h, Refresh bei unbekanntem `kid`; `iss` gegen `tid` gebildet, `aud`, `tid` (Pinning), `exp`/`nbf`/`iat` mit 120 s Toleranz, `nonce` mit `hash_equals` | ✔ 11 Negativtests | +| Benutzerzuordnung | Kein Provisioning; E-Mail lowercase + `is_email`; Domain-Allowlist; Gruppen-Check fail closed; `oid`-Bindung; Multisite-Mitgliedschaft | ✔ | +| Session | `wp_set_auth_cookie` nach Erfolg (neues Session-Token, keine Fixation); `login_redirect`-Filter; `wp_safe_redirect` überall | ✔ | +| Offene Redirects | `redirect_to` → `wp_validate_redirect`; Custom-Login-URL → `wp_validate_redirect` beim Speichern und beim Lesen | ✔ | +| SSRF | Tenant nur GUID oder Whitelist-Wort, `rawurlencode`; Graph-Pfade mit `rawurlencode`; keine benutzerkontrollierten Hosts | ✔ | +| Admin-Oberfläche | `manage_options` überall; Settings-API-Nonce; AJAX `check_ajax_referer` + Capability; Download `check_admin_referer` + Capability; alle Ausgaben `esc_*`; JS nutzt `.text()`/DOM-APIs statt HTML-Strings mit Nutzerdaten | ✔ | +| Eingaben | GUID-Regex, Hex-Farben, Radius-Cap, Icon-URL nur http(s) + Bildendung, Gruppen-IDs GUID, PEM-Größenlimit 20 KB | ✔ | +| Secrets in Logs | Nie geloggt; Token-Fehler nur als Fehlercode; Log nur bei `WP_DEBUG_LOG` | ✔ | +| Nur-Button-Fallback | Key 24 Zeichen/55er-Alphabet (≈139 Bit); Cookie enthält HMAC, nicht den Key; 30 Min.; 10 Versuche/IP/15 Min.; Rotation; Notschalter-Konstante | ✔ | +| Deinstallation | Option, Transients (prepared LIKE), User-Meta, Multisite-Loop | ✔ | +| Abhängigkeiten | Keine externen Bibliotheken, keine CDNs; OpenSSL-Pflicht bei Aktivierung geprüft | ✔ | + +## 5. Empfehlungen für den Betrieb + +1. **Tenant-GUID eintragen** (kein `organizations`/`common`), Domain-Allowlist setzen. +2. **Zertifikat statt Secret** verwenden; Ablauf im Kalender notieren (Backend zeigt Restlaufzeit). +3. In Entra ID **„Zuweisung erforderlich“** für die Enterprise-Anwendung aktivieren und Benutzer/Gruppen zuweisen — zweite Schranke neben der WordPress-Benutzerliste. +4. Gruppen-Beschränkung mit `groups`-Claim *und* Graph-Berechtigung einrichten (Overage-Fall). +5. **Conditional Access / MFA** im Tenant erzwingen; das Plugin erbt die Stärke der Microsoft-Anmeldung. +6. HTTPS mit HSTS; `COOKIE_DOMAIN` korrekt; hinter Proxy `M365_LOGIN_CLIENT_IP_HEADER` setzen. +7. Nur-Button-Modus erst nach erfolgreichem eigenen Microsoft-Login aktivieren; Fallback-Link im Passwortmanager ablegen. +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 + +Sicherheit der Microsoft-Seite (Entra ID, Graph), WordPress-Core, Hosting-Umgebung, andere Plugins/Themes, +Schwachstellen in PHP/OpenSSL. diff --git a/docs/wordpress-org-einreichung.md b/docs/wordpress-org-einreichung.md index f426d1a..946560b 100644 --- a/docs/wordpress-org-einreichung.md +++ b/docs/wordpress-org-einreichung.md @@ -33,6 +33,7 @@ erfüllt. Vor der Einreichung unter graph, 'flush_token' ) ); add_action( 'admin_notices', array( $this, 'setup_notice' ) ); } @@ -140,6 +144,7 @@ class M365_Login_Admin { 'nonce' => wp_create_nonce( self::NONCE_TEST ), 'action' => self::AJAX_TEST, 'groupAction' => self::AJAX_GROUPS, + 'certAction' => self::AJAX_CERT, 'defaultLogo' => M365_Login_Button::microsoft_logo(), 'i18n' => array( 'chooseIcon' => __( 'Choose button icon', 'm365-login' ), @@ -154,6 +159,9 @@ class M365_Login_Admin { 'remove' => __( 'Remove', 'm365-login' ), 'saveFirst' => __( 'Save the connection settings first, then search for groups.', 'm365-login' ), 'confirmKey' => __( 'Generate a new fallback key on save? The old link stops working.', 'm365-login' ), + 'generating' => __( 'Generating a 3072-bit key pair, this takes a moment…', 'm365-login' ), + 'confirmCert' => __( 'Replace the stored certificate? Sign-in stops working until the new certificate is uploaded to Entra ID.', 'm365-login' ), + 'confirmCertRemove' => __( 'Remove the stored certificate when saving? Sign-in with the certificate method stops working.', 'm365-login' ), ), ) ); @@ -224,6 +232,63 @@ class M365_Login_Admin { wp_send_json_success( array( 'groups' => $groups ) ); } + /** + * 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' ) ) { + 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'] ) ) : ''; + if ( 'generate' !== $op ) { + wp_send_json_error( array( 'message' => __( 'Unknown operation.', 'm365-login' ) ) ); + } + + $host = wp_parse_url( home_url(), PHP_URL_HOST ); + $pair = M365_Login_Certificate::generate( is_string( $host ) ? $host : 'wordpress' ); + if ( is_wp_error( $pair ) ) { + wp_send_json_error( array( 'message' => $pair->get_error_message() ) ); + } + $stored = $this->settings->store_certificate( $pair ); + if ( is_wp_error( $stored ) ) { + wp_send_json_error( array( 'message' => $stored->get_error_message() ) ); + } + $this->graph->flush_token(); + + $info = M365_Login_Certificate::info( $pair['certificate'] ); + wp_send_json_success( + array( + 'message' => __( 'Certificate generated and stored. Download the .cer file and upload it in Entra ID.', 'm365-login' ), + 'thumbprint' => $info ? $info['thumbprint'] : '', + ) + ); + } + + /** + * Sends the public certificate as a .cer download (never the private key). + */ + public function download_certificate() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You are not allowed to do this.', 'm365-login' ), 403 ); + } + check_admin_referer( self::POST_CERT ); + + $pem = $this->settings->certificate_pem(); + if ( '' === $pem ) { + wp_die( esc_html__( 'No certificate is stored.', 'm365-login' ), 404 ); + } + $host = wp_parse_url( home_url(), PHP_URL_HOST ); + $name = 'm365-login-' . sanitize_file_name( is_string( $host ) ? $host : 'wordpress' ) . '.cer'; + + nocache_headers(); + header( 'Content-Type: application/x-x509-ca-cert' ); + header( 'Content-Disposition: attachment; filename="' . $name . '"' ); + header( 'Content-Length: ' . strlen( $pem ) ); + echo $pem; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PEM text, public certificate only. + exit; + } + /** * Renders the settings screen. */ @@ -235,6 +300,8 @@ class M365_Login_Admin { $s = $this->settings->all(); $configured = $this->settings->is_configured(); $has_secret = '' !== $this->settings->client_secret(); + $method = $this->settings->auth_method(); + $cert_info = $this->settings->certificate_info(); $option = M365_LOGIN_OPTION; $field = function ( $key ) use ( $option ) { return esc_attr( $option . '[' . $key . ']' ); @@ -280,6 +347,9 @@ class M365_Login_Admin {

+ settings->is_multi_tenant() && '' !== $s['tenant_id'] ) : ?> +

+ @@ -289,18 +359,132 @@ class M365_Login_Admin {
- -
- - -
- -
+ + +
+
+ +
+ + +
+ + + +

+
+ +
> + +
+
    +
  1. +
  2. +
  3. +
  4. +
  5. +
  6. +
+

+
+
+
+ + +
+
+ + + +
+
+
+
+
+
+
Bit RSA
+
+
+
+
+ + + +
+ +

+
+ + +
+ + +

+ +
+ +
> + +
+
    +
  1. +
  2. +
  3. +
  4. +
  5. +
  6. +
+

+

+
+
@@ -580,13 +764,16 @@ class M365_Login_Admin {
-

+

    -
  1. -
  2. -
  3. -
  4. -
  5. +
  6. +
  7. +
  8. +
  9. +
  10. +
  11. +
  12. +

diff --git a/includes/class-m365-login-auth.php b/includes/class-m365-login-auth.php index 9548087..757d9b3 100644 --- a/includes/class-m365-login-auth.php +++ b/includes/class-m365-login-auth.php @@ -179,7 +179,23 @@ class M365_Login_Auth { * @return string */ private function client_ip() { - return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '0.0.0.0'; + $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '0.0.0.0'; + + /** + * Name of a trusted proxy header (e.g. 'HTTP_CF_CONNECTING_IP' or 'HTTP_X_REAL_IP') that carries the + * real client IP. Only set this when every request passes through that proxy; the header is + * client-controlled otherwise. Defaults to the M365_LOGIN_CLIENT_IP_HEADER constant or none. + * + * @param string $header $_SERVER key or ''. + */ + $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] ); + if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) { + $ip = $candidate; + } + } + return $ip; } /* ------------------------------------------------------------------ */ @@ -257,6 +273,14 @@ class M365_Login_Auth { $this->fail( 'not_configured' ); } + // 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 ) { + $this->fail( 'too_many_attempts' ); + } + set_transient( $throttle_key, $starts + 1, self::STATE_TTL ); + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- redirect_to is validated with wp_validate_redirect() before use. $redirect_to = isset( $_GET['redirect_to'] ) ? wp_validate_redirect( esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ), '' ) : ''; @@ -489,19 +513,26 @@ class M365_Login_Auth { * @return array|WP_Error */ private function exchange_code( $code, $verifier ) { + $auth = $this->settings->client_auth_params( $this->token_endpoint() ); + if ( is_wp_error( $auth ) ) { + return $auth; + } + $response = wp_remote_post( $this->token_endpoint(), array( 'timeout' => self::HTTP_TIMEOUT, 'headers' => array( 'Accept' => 'application/json' ), - 'body' => array( - 'client_id' => $this->settings->get( 'client_id' ), - 'client_secret' => $this->settings->client_secret(), - 'grant_type' => 'authorization_code', - 'code' => $code, - 'redirect_uri' => $this->settings->redirect_uri(), - 'code_verifier' => $verifier, - 'scope' => 'openid profile email', + 'body' => array_merge( + array( + 'client_id' => $this->settings->get( 'client_id' ), + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $this->settings->redirect_uri(), + 'code_verifier' => $verifier, + 'scope' => 'openid profile email', + ), + $auth ), ) ); @@ -658,11 +689,26 @@ class M365_Login_Auth { */ private function email_from_claims( $claims ) { $candidates = array(); - if ( ! empty( $claims['email'] ) && is_string( $claims['email'] ) ) { - $candidates[] = $claims['email']; - } - if ( $this->settings->get( 'upn_fallback' ) && ! empty( $claims['preferred_username'] ) && is_string( $claims['preferred_username'] ) ) { - $candidates[] = $claims['preferred_username']; + $email = ! empty( $claims['email'] ) && is_string( $claims['email'] ) ? $claims['email'] : ''; + $upn = ! empty( $claims['preferred_username'] ) && is_string( $claims['preferred_username'] ) ? $claims['preferred_username'] : ''; + + if ( $this->settings->is_multi_tenant() ) { + // In multi-tenant mode any tenant admin can set an arbitrary "email" attribute on their users. + // The UPN domain, on the other hand, must be verified in the issuing tenant, so it comes first; + // the e-mail claim is only used when Microsoft marks its domain as owner-verified (xms_edov). + if ( '' !== $upn ) { + $candidates[] = $upn; + } + if ( '' !== $email && ! empty( $claims['xms_edov'] ) && true === $claims['xms_edov'] ) { + $candidates[] = $email; + } + } else { + if ( '' !== $email ) { + $candidates[] = $email; + } + if ( $this->settings->get( 'upn_fallback' ) && '' !== $upn ) { + $candidates[] = $upn; + } } foreach ( $candidates as $candidate ) { @@ -810,6 +856,7 @@ class M365_Login_Auth { 'group_check_failed' => __( 'Your group membership could not be verified. Please contact an administrator.', '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' ), ); } diff --git a/includes/class-m365-login-certificate.php b/includes/class-m365-login-certificate.php new file mode 100644 index 0000000..fd0653b --- /dev/null +++ b/includes/class-m365-login-certificate.php @@ -0,0 +1,226 @@ + PEM, 'certificate' => PEM ). + */ + public static function generate( $common_name ) { + if ( ! function_exists( 'openssl_pkey_new' ) ) { + return new WP_Error( 'no_openssl', __( 'The PHP OpenSSL extension is not available.', 'm365-login' ) ); + } + + $common_name = preg_replace( '/[^A-Za-z0-9.\-]/', '', (string) $common_name ); + $common_name = '' === $common_name ? 'wordpress' : substr( $common_name, 0, 64 ); + + $key = openssl_pkey_new( + array( + 'private_key_bits' => self::KEY_BITS, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ) + ); + if ( false === $key ) { + return new WP_Error( 'keygen', self::openssl_error( __( 'Key generation failed.', 'm365-login' ) ) ); + } + + $dn = array( + 'CN' => $common_name, + 'O' => 'M365 Login for WordPress', + ); + $csr = openssl_csr_new( $dn, $key, array( 'digest_alg' => 'sha256' ) ); + if ( false === $csr ) { + return new WP_Error( 'csr', self::openssl_error( __( 'Certificate request failed.', 'm365-login' ) ) ); + } + + $cert = openssl_csr_sign( $csr, null, $key, self::VALID_DAYS, array( 'digest_alg' => 'sha256' ), (int) ( time() % PHP_INT_MAX ) ); + if ( false === $cert ) { + return new WP_Error( 'sign', self::openssl_error( __( 'Certificate signing failed.', 'm365-login' ) ) ); + } + + $key_pem = ''; + $cert_pem = ''; + if ( ! openssl_pkey_export( $key, $key_pem ) || ! openssl_x509_export( $cert, $cert_pem ) ) { + return new WP_Error( 'export', self::openssl_error( __( 'Exporting the certificate failed.', 'm365-login' ) ) ); + } + + return array( + 'private_key' => $key_pem, + 'certificate' => $cert_pem, + ); + } + + /** + * Validates a user supplied key/certificate pair. + * + * @param string $key_pem Private key PEM (unencrypted). + * @param string $cert_pem Certificate PEM. + * @return array|WP_Error Normalised pair. + */ + public static function from_pem( $key_pem, $cert_pem ) { + $key_pem = self::normalise_pem( $key_pem ); + $cert_pem = self::normalise_pem( $cert_pem ); + + if ( ! preg_match( '/-----BEGIN (RSA |EC |ENCRYPTED )?PRIVATE KEY-----/', $key_pem ) ) { + return new WP_Error( 'key_format', __( 'The private key must be in PEM format (-----BEGIN PRIVATE KEY-----).', 'm365-login' ) ); + } + if ( false !== strpos( $key_pem, 'ENCRYPTED PRIVATE KEY' ) ) { + return new WP_Error( 'key_encrypted', __( 'Password protected private keys are not supported. Export the key without a passphrase.', 'm365-login' ) ); + } + $key = openssl_pkey_get_private( $key_pem ); + if ( false === $key ) { + return new WP_Error( 'key_invalid', self::openssl_error( __( 'The private key could not be read.', 'm365-login' ) ) ); + } + $details = openssl_pkey_get_details( $key ); + if ( ! is_array( $details ) || OPENSSL_KEYTYPE_RSA !== $details['type'] ) { + return new WP_Error( 'key_type', __( 'Only RSA keys are supported.', 'm365-login' ) ); + } + if ( $details['bits'] < 2048 ) { + return new WP_Error( 'key_bits', __( 'The RSA key must have at least 2048 bits.', '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' ) ) ); + } + if ( ! openssl_x509_check_private_key( $cert, $key ) ) { + return new WP_Error( 'cert_mismatch', __( 'The certificate does not belong to this private key.', 'm365-login' ) ); + } + + $info = openssl_x509_parse( $cert ); + if ( is_array( $info ) && ! empty( $info['validTo_time_t'] ) && (int) $info['validTo_time_t'] < time() ) { + return new WP_Error( 'cert_expired', __( 'The certificate has already expired.', 'm365-login' ) ); + } + + return array( + 'private_key' => $key_pem, + 'certificate' => $cert_pem, + ); + } + + /** + * Normalises line endings and trims a PEM block. + * + * @param string $pem PEM. + * @return string + */ + private static function normalise_pem( $pem ) { + $pem = str_replace( array( "\r\n", "\r" ), "\n", trim( (string) $pem ) ); + return $pem . "\n"; + } + + /** + * Certificate metadata for display. + * + * @param string $cert_pem Certificate PEM. + * @return array|null array( 'thumbprint' => hex SHA-1, 'thumbprint_sha256' => hex, 'subject' => string, 'not_before' => ts, 'not_after' => ts, 'bits' => int ). + */ + public static function info( $cert_pem ) { + if ( '' === (string) $cert_pem ) { + return null; + } + $cert = openssl_x509_read( $cert_pem ); + if ( false === $cert ) { + return null; + } + $parsed = openssl_x509_parse( $cert ); + $der = self::der( $cert_pem ); + $public = openssl_pkey_get_public( $cert ); + $detail = $public ? openssl_pkey_get_details( $public ) : null; + + return array( + 'thumbprint' => strtoupper( sha1( $der ) ), + 'thumbprint_sha256' => strtoupper( hash( 'sha256', $der ) ), + 'subject' => isset( $parsed['subject']['CN'] ) ? (string) $parsed['subject']['CN'] : '', + 'not_before' => isset( $parsed['validFrom_time_t'] ) ? (int) $parsed['validFrom_time_t'] : 0, + 'not_after' => isset( $parsed['validTo_time_t'] ) ? (int) $parsed['validTo_time_t'] : 0, + 'bits' => is_array( $detail ) && isset( $detail['bits'] ) ? (int) $detail['bits'] : 0, + ); + } + + /** + * DER bytes of a PEM certificate. + * + * @param string $cert_pem PEM. + * @return string + */ + private static function der( $cert_pem ) { + $body = preg_replace( '/-----[^-]+-----|\s+/', '', (string) $cert_pem ); + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode + $der = base64_decode( $body, true ); + return false === $der ? '' : $der; + } + + /** + * Builds the signed client assertion for the token endpoint. + * + * @param string $key_pem Private key PEM. + * @param string $cert_pem Certificate PEM (for the x5t header). + * @param string $client_id Application (client) ID. + * @param string $token_endpoint Audience. + * @return string|WP_Error Compact JWS. + */ + public static function assertion( $key_pem, $cert_pem, $client_id, $token_endpoint ) { + $key = openssl_pkey_get_private( $key_pem ); + if ( false === $key ) { + return new WP_Error( 'key_invalid', 'Private key could not be loaded.' ); + } + $der = self::der( $cert_pem ); + if ( '' === $der ) { + return new WP_Error( 'cert_invalid', 'Certificate could not be decoded.' ); + } + + $now = time(); + $header = array( + 'alg' => 'RS256', + 'typ' => 'JWT', + 'x5t' => M365_Login_JWT::b64url_encode( sha1( $der, true ) ), + 'x5t#S256' => M365_Login_JWT::b64url_encode( hash( 'sha256', $der, true ) ), + ); + $claims = array( + 'aud' => $token_endpoint, + 'iss' => $client_id, + 'sub' => $client_id, + 'jti' => M365_Login_JWT::b64url_encode( random_bytes( 24 ) ), + 'nbf' => $now - 30, + 'iat' => $now, + 'exp' => $now + self::ASSERTION_TTL, + ); + + $signing_input = M365_Login_JWT::b64url_encode( wp_json_encode( $header ) ) . '.' . M365_Login_JWT::b64url_encode( wp_json_encode( $claims ) ); + $signature = ''; + if ( ! openssl_sign( $signing_input, $signature, $key, OPENSSL_ALGO_SHA256 ) ) { + return new WP_Error( 'sign', 'Signing the client assertion failed.' ); + } + return $signing_input . '.' . M365_Login_JWT::b64url_encode( $signature ); + } + + /** + * Prefixes the last OpenSSL error to a message (for admins). + * + * @param string $message Message. + * @return string + */ + private static function openssl_error( $message ) { + $detail = openssl_error_string(); + return $detail ? $message . ' (' . $detail . ')' : $message; + } +} diff --git a/includes/class-m365-login-graph.php b/includes/class-m365-login-graph.php index 28bfb4d..ba1f6e5 100644 --- a/includes/class-m365-login-graph.php +++ b/includes/class-m365-login-graph.php @@ -38,7 +38,7 @@ class M365_Login_Graph { * @return string */ private function token_cache_key() { - return 'm365_login_apptoken_' . md5( $this->settings->tenant() . '|' . $this->settings->get( 'client_id' ) ); + return 'm365_login_apptoken_' . md5( $this->settings->tenant() . '|' . $this->settings->get( 'client_id' ) . '|' . $this->settings->auth_method() ); } /** @@ -63,16 +63,24 @@ class M365_Login_Graph { return new WP_Error( 'graph_not_configured', __( 'Microsoft login is not configured yet.', 'm365-login' ) ); } + $token_endpoint = 'https://login.microsoftonline.com/' . rawurlencode( $this->settings->tenant() ) . '/oauth2/v2.0/token'; + $auth = $this->settings->client_auth_params( $token_endpoint ); + if ( is_wp_error( $auth ) ) { + return $auth; + } + $response = wp_remote_post( - 'https://login.microsoftonline.com/' . rawurlencode( $this->settings->tenant() ) . '/oauth2/v2.0/token', + $token_endpoint, array( 'timeout' => self::HTTP_TIMEOUT, 'headers' => array( 'Accept' => 'application/json' ), - 'body' => array( - 'client_id' => $this->settings->get( 'client_id' ), - 'client_secret' => $this->settings->client_secret(), - 'grant_type' => 'client_credentials', - 'scope' => 'https://graph.microsoft.com/.default', + 'body' => array_merge( + array( + 'client_id' => $this->settings->get( 'client_id' ), + 'grant_type' => 'client_credentials', + 'scope' => 'https://graph.microsoft.com/.default', + ), + $auth ), ) ); diff --git a/includes/class-m365-login-settings.php b/includes/class-m365-login-settings.php index e5bfc8c..8fcbfaf 100644 --- a/includes/class-m365-login-settings.php +++ b/includes/class-m365-login-settings.php @@ -30,6 +30,9 @@ class M365_Login_Settings { 'tenant_id' => '', 'client_id' => '', 'client_secret' => '', // Stored encrypted. + 'auth_method' => 'secret', // 'secret' or 'certificate'. + 'cert_private_key' => '', // PEM, stored encrypted. + 'cert_certificate' => '', // PEM (public). 'prompt' => 'select_account', // Security / matching. 'upn_fallback' => 1, @@ -96,13 +99,120 @@ class M365_Login_Settings { return is_string( $plain ) ? $plain : ''; } + /** + * Selected client authentication method. + * + * @return string 'secret' or 'certificate'. + */ + public function auth_method() { + return 'certificate' === $this->get( 'auth_method' ) ? 'certificate' : 'secret'; + } + + /** + * Decrypted certificate private key (PEM) or ''. + * + * @return string + */ + public function certificate_key() { + $enc = (string) $this->get( 'cert_private_key', '' ); + if ( '' === $enc ) { + return ''; + } + $plain = M365_Login_Crypto::decrypt( $enc ); + return is_string( $plain ) ? $plain : ''; + } + + /** + * Certificate PEM (public part) or ''. + * + * @return string + */ + public function certificate_pem() { + return (string) $this->get( 'cert_certificate', '' ); + } + + /** + * Whether a usable certificate + key pair is stored. + * + * @return bool + */ + public function has_certificate() { + return '' !== $this->certificate_pem() && '' !== $this->certificate_key(); + } + + /** + * Parsed certificate metadata or null. + * + * @return array|null + */ + public function certificate_info() { + return $this->has_certificate() ? M365_Login_Certificate::info( $this->certificate_pem() ) : null; + } + /** * Whether the plugin has everything it needs to start a login. * * @return bool */ public function is_configured() { - return '' !== $this->get( 'tenant_id' ) && '' !== $this->get( 'client_id' ) && '' !== $this->client_secret(); + if ( '' === $this->get( 'tenant_id' ) || '' === $this->get( 'client_id' ) ) { + return false; + } + if ( 'certificate' === $this->auth_method() ) { + $info = $this->certificate_info(); + return null !== $info && ( 0 === $info['not_after'] || $info['not_after'] > time() ); + } + return '' !== $this->client_secret(); + } + + /** + * Client authentication parameters for the token endpoint (secret or signed assertion). + * + * @param string $token_endpoint Token endpoint URL (assertion audience). + * @return array|WP_Error + */ + public function client_auth_params( $token_endpoint ) { + if ( 'certificate' === $this->auth_method() ) { + $assertion = M365_Login_Certificate::assertion( $this->certificate_key(), $this->certificate_pem(), (string) $this->get( 'client_id' ), $token_endpoint ); + if ( is_wp_error( $assertion ) ) { + return $assertion; + } + return array( + 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + 'client_assertion' => $assertion, + ); + } + return array( 'client_secret' => $this->client_secret() ); + } + + /** + * Stores a validated key/certificate pair (key encrypted). + * + * @param array $pair array( 'private_key' => PEM, 'certificate' => PEM ). + * @return true|WP_Error + */ + public function store_certificate( $pair ) { + $enc = M365_Login_Crypto::encrypt( $pair['private_key'] ); + if ( false === $enc ) { + return new WP_Error( 'encrypt', __( 'The private key could not be encrypted. Is the OpenSSL extension available?', 'm365-login' ) ); + } + $all = $this->all(); + $all['cert_private_key'] = $enc; + $all['cert_certificate'] = $pair['certificate']; + update_option( M365_LOGIN_OPTION, $all ); + $this->cache = null; + return true; + } + + /** + * Removes the stored certificate and key. + */ + public function remove_certificate() { + $all = $this->all(); + $all['cert_private_key'] = ''; + $all['cert_certificate'] = ''; + update_option( M365_LOGIN_OPTION, $all ); + $this->cache = null; } /** @@ -115,6 +225,15 @@ class M365_Login_Settings { return '' === $tenant ? 'organizations' : $tenant; } + /** + * Whether sign-ins from more than one tenant are accepted (no tenant GUID pinned). + * + * @return bool + */ + public function is_multi_tenant() { + return ! self::is_guid( $this->tenant() ); + } + /** * Redirect URI registered in Entra ID. * @@ -300,6 +419,41 @@ class M365_Login_Settings { } } + $method = isset( $input['auth_method'] ) ? sanitize_key( $input['auth_method'] ) : 'secret'; + $out['auth_method'] = 'certificate' === $method ? 'certificate' : 'secret'; + + // 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'] ) ) : ''; + if ( ! empty( $input['cert_remove'] ) ) { + $out['cert_private_key'] = ''; + $out['cert_certificate'] = ''; + } elseif ( '' !== $pasted_key || '' !== $pasted_cert ) { + if ( '' === $pasted_key || '' === $pasted_cert ) { + add_settings_error( M365_LOGIN_OPTION, 'certificate', __( 'Please paste both the private key and the certificate.', 'm365-login' ) ); + } elseif ( strlen( $pasted_key ) > 20000 || strlen( $pasted_cert ) > 20000 ) { + add_settings_error( M365_LOGIN_OPTION, 'certificate', __( 'The pasted key or certificate is too large.', 'm365-login' ) ); + } else { + $pair = M365_Login_Certificate::from_pem( $pasted_key, $pasted_cert ); + if ( is_wp_error( $pair ) ) { + add_settings_error( M365_LOGIN_OPTION, 'certificate', $pair->get_error_message() ); + } else { + $enc = M365_Login_Crypto::encrypt( $pair['private_key'] ); + if ( false === $enc ) { + add_settings_error( M365_LOGIN_OPTION, 'certificate', __( 'The private key could not be encrypted. Is the OpenSSL extension available?', 'm365-login' ) ); + } else { + $out['cert_private_key'] = $enc; + $out['cert_certificate'] = $pair['certificate']; + } + } + } + } + if ( 'certificate' === $out['auth_method'] && '' === $out['cert_certificate'] ) { + add_settings_error( M365_LOGIN_OPTION, 'auth_method', __( 'Certificate authentication is selected but no certificate is stored yet. Generate one or paste your own; the Microsoft button stays hidden until then.', 'm365-login' ), 'warning' ); + } + $prompt = isset( $input['prompt'] ) ? sanitize_key( $input['prompt'] ) : ''; $out['prompt'] = in_array( $prompt, array( 'none', 'select_account', 'login' ), true ) ? $prompt : 'none'; diff --git a/languages/m365-login-de_DE.mo b/languages/m365-login-de_DE.mo index 66c78c1..36e39e2 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 c12aec7..12a009f 100644 --- a/languages/m365-login-de_DE.po +++ b/languages/m365-login-de_DE.po @@ -16,511 +16,709 @@ msgstr "" "X-Generator: bin/make-pot.py\n" "X-Domain: m365-login\n" -#: includes/class-m365-login-admin.php:75 includes/class-m365-login-admin.php:76 includes/class-m365-login-admin.php:248 +#: includes/class-m365-login-admin.php:79 includes/class-m365-login-admin.php:80 includes/class-m365-login-admin.php:315 msgid "M365 Login" msgstr "M365 Login" -#: includes/class-m365-login-admin.php:114 +#: includes/class-m365-login-admin.php:118 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:116 +#: includes/class-m365-login-admin.php:120 msgid "Open the settings" msgstr "Einstellungen öffnen" -#: includes/class-m365-login-admin.php:145 +#: includes/class-m365-login-admin.php:150 msgid "Choose button icon" msgstr "Button-Icon auswählen" -#: includes/class-m365-login-admin.php:146 +#: includes/class-m365-login-admin.php:151 msgid "Use this icon" msgstr "Dieses Icon verwenden" -#: includes/class-m365-login-admin.php:147 +#: includes/class-m365-login-admin.php:152 msgid "Copied!" msgstr "Kopiert!" -#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:529 includes/class-m365-login-admin.php:572 +#: includes/class-m365-login-admin.php:153 includes/class-m365-login-admin.php:435 includes/class-m365-login-admin.php:713 includes/class-m365-login-admin.php:756 msgid "Copy" msgstr "Kopieren" -#: includes/class-m365-login-admin.php:149 +#: includes/class-m365-login-admin.php:154 msgid "Testing…" msgstr "Wird geprüft …" -#: includes/class-m365-login-admin.php:150 +#: includes/class-m365-login-admin.php:155 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:151 +#: includes/class-m365-login-admin.php:156 msgid "No groups found." msgstr "Keine Gruppen gefunden." -#: includes/class-m365-login-admin.php:152 +#: includes/class-m365-login-admin.php:157 msgid "Searching…" msgstr "Suche läuft …" -#: includes/class-m365-login-admin.php:153 +#: includes/class-m365-login-admin.php:158 msgid "Add" msgstr "Hinzufügen" -#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:503 +#: includes/class-m365-login-admin.php:159 includes/class-m365-login-admin.php:687 msgid "Remove" msgstr "Entfernen" -#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:488 +#: includes/class-m365-login-admin.php:160 includes/class-m365-login-admin.php:218 includes/class-m365-login-admin.php:672 msgid "Save the connection settings first, then search for groups." msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen." -#: includes/class-m365-login-admin.php:156 +#: includes/class-m365-login-admin.php:161 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:168 includes/class-m365-login-admin.php:207 +#: includes/class-m365-login-admin.php:162 +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:163 +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:164 +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:176 includes/class-m365-login-admin.php:215 includes/class-m365-login-admin.php:241 includes/class-m365-login-admin.php:273 msgid "You are not allowed to do this." msgstr "Dafür fehlt die Berechtigung." -#: includes/class-m365-login-admin.php:173 +#: includes/class-m365-login-admin.php:181 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:187 +#: includes/class-m365-login-admin.php:195 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:196 +#: includes/class-m365-login-admin.php:204 msgid "Tenant reachable. The OpenID configuration was loaded successfully." msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen." -#: includes/class-m365-login-admin.php:219 +#: includes/class-m365-login-admin.php:227 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:232 +#: includes/class-m365-login-admin.php:245 +msgid "Unknown operation." +msgstr "Unbekannte Aktion." + +#: includes/class-m365-login-admin.php:262 +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:279 +msgid "No certificate is stored." +msgstr "Es ist kein Zertifikat gespeichert." + +#: includes/class-m365-login-admin.php:297 msgid "You are not allowed to access this page." msgstr "Für diese Seite fehlt die Berechtigung." -#: includes/class-m365-login-admin.php:249 +#: includes/class-m365-login-admin.php:316 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:254 +#: includes/class-m365-login-admin.php:321 msgid "Connected" msgstr "Verbunden" -#: includes/class-m365-login-admin.php:254 +#: includes/class-m365-login-admin.php:321 msgid "Setup incomplete" msgstr "Einrichtung unvollständig" -#: includes/class-m365-login-admin.php:262 +#: includes/class-m365-login-admin.php:329 msgid "Connection" msgstr "Verbindung" -#: includes/class-m365-login-admin.php:263 +#: includes/class-m365-login-admin.php:330 msgid "Button" msgstr "Button" -#: includes/class-m365-login-admin.php:264 +#: includes/class-m365-login-admin.php:331 msgid "Security" msgstr "Sicherheit" -#: includes/class-m365-login-admin.php:273 +#: includes/class-m365-login-admin.php:340 msgid "Microsoft Entra ID app registration" msgstr "App-Registrierung in Microsoft Entra ID" -#: includes/class-m365-login-admin.php:274 +#: includes/class-m365-login-admin.php:341 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:277 +#: includes/class-m365-login-admin.php:344 msgid "Directory (tenant) ID" msgstr "Verzeichnis-ID (Mandant/Tenant)" -#: includes/class-m365-login-admin.php:280 +#: includes/class-m365-login-admin.php:347 msgid "Test tenant" msgstr "Tenant testen" -#: includes/class-m365-login-admin.php:282 +#: includes/class-m365-login-admin.php:349 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:287 +#: includes/class-m365-login-admin.php:351 +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:357 msgid "Application (client) ID" msgstr "Anwendungs-ID (Client)" -#: includes/class-m365-login-admin.php:292 +#: includes/class-m365-login-admin.php:362 +msgid "How should WordPress authenticate to Microsoft?" +msgstr "Wie soll sich WordPress bei Microsoft authentifizieren?" + +#: includes/class-m365-login-admin.php:367 includes/class-m365-login-admin.php:384 msgid "Client secret" msgstr "Geheimer Clientschlüssel (Client Secret)" -#: includes/class-m365-login-admin.php:294 +#: includes/class-m365-login-admin.php:368 +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:374 +msgid "Certificate" +msgstr "Zertifikat" + +#: includes/class-m365-login-admin.php:374 +msgid "Recommended" +msgstr "Empfohlen" + +#: includes/class-m365-login-admin.php:375 +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:386 msgid "•••••••••••• (stored, leave empty to keep)" msgstr "•••••••••••• (gespeichert – leer lassen, um zu behalten)" -#: includes/class-m365-login-admin.php:294 +#: includes/class-m365-login-admin.php:386 msgid "Paste the secret value" msgstr "Wert des Secrets einfügen" -#: includes/class-m365-login-admin.php:295 +#: includes/class-m365-login-admin.php:387 msgid "Show secret" msgstr "Secret anzeigen" -#: includes/class-m365-login-admin.php:300 +#: includes/class-m365-login-admin.php:392 msgid "Remove the stored secret" msgstr "Gespeichertes Secret entfernen" -#: includes/class-m365-login-admin.php:303 +#: includes/class-m365-login-admin.php:395 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:307 +#: includes/class-m365-login-admin.php:399 +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:402 +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:403 +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:404 +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:405 +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:406 +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:407 +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:409 +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:422 +msgid "Expired" +msgstr "Abgelaufen" + +#. translators: %d: number of days +#: includes/class-m365-login-admin.php:426 +msgid "Expires in %d days" +msgstr "Läuft in %d Tagen ab" + +#. translators: %d: number of days +#: includes/class-m365-login-admin.php:429 +msgid "Valid" +msgstr "Gültig" + +#: includes/class-m365-login-admin.php:434 +msgid "Thumbprint (SHA-1)" +msgstr "Fingerabdruck (SHA-1)" + +#: includes/class-m365-login-admin.php:436 +msgid "Subject" +msgstr "Antragsteller" + +#: includes/class-m365-login-admin.php:438 +msgid "Key size" +msgstr "Schlüssellänge" + +#: includes/class-m365-login-admin.php:440 +msgid "Valid until" +msgstr "Gültig bis" + +#: includes/class-m365-login-admin.php:444 +msgid "Download certificate (.cer)" +msgstr "Zertifikat herunterladen (.cer)" + +#: includes/class-m365-login-admin.php:445 +msgid "Generate new certificate" +msgstr "Neues Zertifikat erzeugen" + +#: includes/class-m365-login-admin.php:448 +msgid "Remove certificate when saving" +msgstr "Zertifikat beim Speichern entfernen" + +#: includes/class-m365-login-admin.php:452 +msgid "No certificate stored yet." +msgstr "Noch kein Zertifikat gespeichert." + +#: includes/class-m365-login-admin.php:454 +msgid "Generate certificate" +msgstr "Zertifikat erzeugen" + +#: includes/class-m365-login-admin.php:455 +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:459 +msgid "Use your own certificate instead (paste PEM)" +msgstr "Stattdessen eigenes Zertifikat verwenden (PEM einfügen)" + +#: includes/class-m365-login-admin.php:462 +msgid "Private key (PEM, unencrypted)" +msgstr "Privater Schlüssel (PEM, unverschlüsselt)" + +#: includes/class-m365-login-admin.php:466 +msgid "Certificate (PEM)" +msgstr "Zertifikat (PEM)" + +#: includes/class-m365-login-admin.php:468 +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:474 +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:477 +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:478 +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:479 +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:480 +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:481 +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:482 +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:484 +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:485 +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:491 msgid "Account prompt" msgstr "Kontoauswahl" -#: includes/class-m365-login-admin.php:309 +#: includes/class-m365-login-admin.php:493 msgid "Always let the user pick an account (recommended)" msgstr "Benutzer wählt immer ein Konto aus (empfohlen)" -#: includes/class-m365-login-admin.php:310 +#: includes/class-m365-login-admin.php:494 msgid "Use the current Microsoft session if available" msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden" -#: includes/class-m365-login-admin.php:311 +#: includes/class-m365-login-admin.php:495 msgid "Always require re-entering credentials" msgstr "Immer erneute Eingabe der Anmeldedaten verlangen" -#: includes/class-m365-login-admin.php:320 +#: includes/class-m365-login-admin.php:504 msgid "Appearance" msgstr "Darstellung" -#: includes/class-m365-login-admin.php:323 +#: includes/class-m365-login-admin.php:507 msgid "Live preview" msgstr "Live-Vorschau" -#: includes/class-m365-login-admin.php:337 +#: includes/class-m365-login-admin.php:521 msgid "Button text" msgstr "Button-Text" -#: includes/class-m365-login-admin.php:341 +#: includes/class-m365-login-admin.php:525 msgid "Divider text" msgstr "Trennlinien-Text" -#: includes/class-m365-login-admin.php:343 +#: includes/class-m365-login-admin.php:527 msgid "Leave empty to hide the divider line." msgstr "Leer lassen, um die Trennlinie auszublenden." -#: includes/class-m365-login-admin.php:348 +#: includes/class-m365-login-admin.php:532 msgid "Icon" msgstr "Icon" -#: includes/class-m365-login-admin.php:351 +#: includes/class-m365-login-admin.php:535 msgid "Show an icon on the button" msgstr "Icon auf dem Button anzeigen" -#: includes/class-m365-login-admin.php:362 +#: includes/class-m365-login-admin.php:546 msgid "Default: Microsoft logo" msgstr "Standard: Microsoft-Logo" -#: includes/class-m365-login-admin.php:364 +#: includes/class-m365-login-admin.php:548 msgid "Choose from media library" msgstr "Aus Mediathek wählen" -#: includes/class-m365-login-admin.php:365 +#: includes/class-m365-login-admin.php:549 msgid "Use Microsoft logo" msgstr "Microsoft-Logo verwenden" -#: includes/class-m365-login-admin.php:367 +#: includes/class-m365-login-admin.php:551 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:375 +#: includes/class-m365-login-admin.php:559 msgid "Background" msgstr "Hintergrund" -#: includes/class-m365-login-admin.php:376 +#: includes/class-m365-login-admin.php:560 msgid "Background (hover)" msgstr "Hintergrund (Hover)" -#: includes/class-m365-login-admin.php:377 +#: includes/class-m365-login-admin.php:561 msgid "Text colour" msgstr "Textfarbe" -#: includes/class-m365-login-admin.php:378 +#: includes/class-m365-login-admin.php:562 msgid "Border" msgstr "Rahmen" -#: includes/class-m365-login-admin.php:391 +#: includes/class-m365-login-admin.php:575 msgid "Corner radius" msgstr "Eckenradius" -#: includes/class-m365-login-admin.php:395 +#: includes/class-m365-login-admin.php:579 msgid "Position on the login page" msgstr "Position auf der Login-Seite" -#: includes/class-m365-login-admin.php:397 +#: includes/class-m365-login-admin.php:581 msgid "Below the login form" msgstr "Unter dem Login-Formular" -#: includes/class-m365-login-admin.php:398 +#: includes/class-m365-login-admin.php:582 msgid "Above the login form" msgstr "Über dem Login-Formular" -#: includes/class-m365-login-admin.php:404 +#: includes/class-m365-login-admin.php:588 msgid "Quick presets" msgstr "Schnellauswahl" -#: includes/class-m365-login-admin.php:405 +#: includes/class-m365-login-admin.php:589 msgid "Microsoft dark" msgstr "Microsoft dunkel" -#: includes/class-m365-login-admin.php:406 +#: includes/class-m365-login-admin.php:590 msgid "Microsoft light" msgstr "Microsoft hell" -#: includes/class-m365-login-admin.php:407 +#: includes/class-m365-login-admin.php:591 msgid "Azure blue" msgstr "Azure-Blau" -#: includes/class-m365-login-admin.php:408 +#: includes/class-m365-login-admin.php:592 msgid "WordPress blue" msgstr "WordPress-Blau" -#: includes/class-m365-login-admin.php:412 +#: includes/class-m365-login-admin.php:596 msgid "Custom login page" msgstr "Eigene Login-Seite" -#: includes/class-m365-login-admin.php:413 +#: includes/class-m365-login-admin.php:597 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:416 +#: includes/class-m365-login-admin.php:600 msgid "URL of your login page" msgstr "URL der Login-Seite" -#: includes/class-m365-login-admin.php:418 +#: includes/class-m365-login-admin.php:602 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:424 +#: includes/class-m365-login-admin.php:608 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:425 +#: includes/class-m365-login-admin.php:609 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:430 +#: includes/class-m365-login-admin.php:614 msgid "Manual placement" msgstr "Manuelle Platzierung" -#: includes/class-m365-login-admin.php:431 +#: includes/class-m365-login-admin.php:615 msgid "Shortcode (block editor, page builders):" msgstr "Shortcode (Block-Editor, Page Builder):" -#: includes/class-m365-login-admin.php:433 +#: includes/class-m365-login-admin.php:617 msgid "Template function (theme files):" msgstr "Template-Funktion (Theme-Dateien):" -#: includes/class-m365-login-admin.php:435 +#: includes/class-m365-login-admin.php:619 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:443 +#: includes/class-m365-login-admin.php:627 msgid "User matching & hardening" msgstr "Benutzerzuordnung & Härtung" -#: includes/class-m365-login-admin.php:444 +#: includes/class-m365-login-admin.php:628 msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists." msgstr "Benutzer werden nie automatisch angelegt. Eine Microsoft-Anmeldung gelingt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert." -#: includes/class-m365-login-admin.php:449 +#: includes/class-m365-login-admin.php:633 msgid "Bind WordPress accounts to the Microsoft object ID" msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden" -#: includes/class-m365-login-admin.php:450 +#: includes/class-m365-login-admin.php:634 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:457 +#: includes/class-m365-login-admin.php:641 msgid "Fall back to the user principal name (UPN)" msgstr "Auf den User Principal Name (UPN) zurückgreifen" -#: includes/class-m365-login-admin.php:458 +#: includes/class-m365-login-admin.php:642 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:465 +#: includes/class-m365-login-admin.php:649 msgid "Keep users signed in (\"Remember me\")" msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)" -#: includes/class-m365-login-admin.php:466 +#: includes/class-m365-login-admin.php:650 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:471 +#: includes/class-m365-login-admin.php:655 msgid "Allowed e-mail domains (optional)" msgstr "Erlaubte E-Mail-Domains (optional)" -#: includes/class-m365-login-admin.php:473 +#: includes/class-m365-login-admin.php:657 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:478 +#: includes/class-m365-login-admin.php:662 msgid "Allowed Entra groups (optional)" msgstr "Erlaubte Entra-Gruppen (optional)" -#: includes/class-m365-login-admin.php:479 +#: includes/class-m365-login-admin.php:663 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:482 +#: includes/class-m365-login-admin.php:666 msgid "Search groups" msgstr "Gruppen suchen" -#: includes/class-m365-login-admin.php:484 +#: includes/class-m365-login-admin.php:668 msgid "Type a group name or paste an object ID…" msgstr "Gruppenname eingeben oder Objekt-ID einfügen …" -#: includes/class-m365-login-admin.php:485 +#: includes/class-m365-login-admin.php:669 msgid "Search" msgstr "Suchen" -#: includes/class-m365-login-admin.php:490 +#: includes/class-m365-login-admin.php:674 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:496 +#: includes/class-m365-login-admin.php:680 msgid "Selected groups" msgstr "Ausgewählte Gruppen" -#: includes/class-m365-login-admin.php:497 +#: includes/class-m365-login-admin.php:681 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:507 +#: includes/class-m365-login-admin.php:691 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:512 +#: includes/class-m365-login-admin.php:696 msgid "Button-only mode" msgstr "Nur-Button-Modus" -#: includes/class-m365-login-admin.php:513 +#: includes/class-m365-login-admin.php:697 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:518 +#: includes/class-m365-login-admin.php:702 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:519 +#: includes/class-m365-login-admin.php:703 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:524 +#: includes/class-m365-login-admin.php:708 msgid "Fallback link (keep it secret)" msgstr "Fallback-Link (geheim halten)" -#: includes/class-m365-login-admin.php:525 +#: includes/class-m365-login-admin.php:709 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:533 +#: includes/class-m365-login-admin.php:717 msgid "Generate a new key when saving" msgstr "Beim Speichern einen neuen Schlüssel erzeugen" -#: includes/class-m365-login-admin.php:536 +#: includes/class-m365-login-admin.php:720 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:542 +#: includes/class-m365-login-admin.php:726 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:551 +#: includes/class-m365-login-admin.php:735 msgid "What the plugin does to keep sign-ins safe" msgstr "So schützt das Plugin die Anmeldung" -#: includes/class-m365-login-admin.php:553 +#: includes/class-m365-login-admin.php:737 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:554 +#: includes/class-m365-login-admin.php:738 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:555 +#: includes/class-m365-login-admin.php:739 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:556 +#: includes/class-m365-login-admin.php:740 msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed." msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert." -#: includes/class-m365-login-admin.php:562 +#: includes/class-m365-login-admin.php:746 msgid "Save changes" msgstr "Änderungen speichern" -#: includes/class-m365-login-admin.php:568 +#: includes/class-m365-login-admin.php:752 msgid "Redirect URI" msgstr "Umleitungs-URI (Redirect URI)" -#: includes/class-m365-login-admin.php:569 +#: includes/class-m365-login-admin.php:753 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:575 +#: includes/class-m365-login-admin.php:759 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:578 +#: includes/class-m365-login-admin.php:762 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:583 -msgid "Setup in 5 steps" -msgstr "Einrichtung in 5 Schritten" +#: includes/class-m365-login-admin.php:767 +msgid "Setup guide: app registration" +msgstr "Anleitung: App-Registrierung" -#: includes/class-m365-login-admin.php:585 -msgid "Open the Microsoft Entra admin center → App registrations → New registration." -msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung." +#: includes/class-m365-login-admin.php:769 +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:586 -msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above." -msgstr "„Nur Konten in diesem Organisationsverzeichnis“ wählen, Plattform „Web“ auswählen und die Umleitungs-URI von oben einfügen." +#: includes/class-m365-login-admin.php:770 +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:587 -msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page." -msgstr "Anwendungs-ID (Client) und Verzeichnis-ID (Mandant) von der Übersichtsseite kopieren." +#: includes/class-m365-login-admin.php:771 +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:588 -msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)." -msgstr "Unter „Zertifikate & Geheimnisse“ einen geheimen Clientschlüssel erstellen und dessen Wert (nicht die ID) kopieren." +#: includes/class-m365-login-admin.php:772 +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:589 -msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page." -msgstr "Unter „Tokenkonfiguration“ den optionalen Anspruch „email“ für ID-Tokens hinzufügen (empfohlen), dann diese Seite speichern." +#: includes/class-m365-login-admin.php:773 +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:591 +#: includes/class-m365-login-admin.php:774 +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:775 +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:776 +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:778 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:592 +#: includes/class-m365-login-admin.php:779 msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent." msgstr "Optional für Gruppen-Beschränkungen: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung." -#: includes/class-m365-login-admin.php:596 +#: includes/class-m365-login-admin.php:783 msgid "Shortcode" msgstr "Shortcode" -#: includes/class-m365-login-admin.php:597 +#: includes/class-m365-login-admin.php:784 msgid "Place the button on a custom login page:" msgstr "Button auf einer eigenen Login-Seite platzieren:" -#: includes/class-m365-login-admin.php:599 +#: includes/class-m365-login-admin.php:786 msgid "More options on the Button tab under \"Custom login page\"." msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“." @@ -528,107 +726,179 @@ msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“." 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:777 +#: includes/class-m365-login-auth.php:823 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:798 includes/class-m365-login-graph.php:63 +#: includes/class-m365-login-auth.php:844 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:799 +#: includes/class-m365-login-auth.php:845 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:800 +#: includes/class-m365-login-auth.php:846 msgid "Microsoft sign-in was cancelled." msgstr "Die Microsoft-Anmeldung wurde abgebrochen." -#: includes/class-m365-login-auth.php:801 +#: includes/class-m365-login-auth.php:847 msgid "Microsoft returned an error. Please try again." msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen." -#: includes/class-m365-login-auth.php:802 +#: includes/class-m365-login-auth.php:848 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:803 +#: includes/class-m365-login-auth.php:849 msgid "The Microsoft sign-in could not be verified." msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden." -#: includes/class-m365-login-auth.php:804 +#: includes/class-m365-login-auth.php:850 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:805 +#: includes/class-m365-login-auth.php:851 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:806 +#: includes/class-m365-login-auth.php:852 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:807 +#: includes/class-m365-login-auth.php:853 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:808 +#: includes/class-m365-login-auth.php:854 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:809 +#: includes/class-m365-login-auth.php:855 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:810 +#: includes/class-m365-login-auth.php:856 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:811 +#: includes/class-m365-login-auth.php:857 msgid "The fallback key is not valid." msgstr "Der Fallback-Schlüssel ist ungültig." -#: includes/class-m365-login-auth.php:812 +#: includes/class-m365-login-auth.php:858 msgid "Too many attempts. Please wait 15 minutes." msgstr "Zu viele Versuche. Bitte 15 Minuten warten." -#: includes/class-m365-login-graph.php:193 +#: includes/class-m365-login-auth.php:859 +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-certificate.php:29 +msgid "The PHP OpenSSL extension is not available." +msgstr "Die PHP-Erweiterung OpenSSL ist nicht verfügbar." + +#: includes/class-m365-login-certificate.php:42 +msgid "Key generation failed." +msgstr "Die Schlüsselerzeugung ist fehlgeschlagen." + +#: includes/class-m365-login-certificate.php:51 +msgid "Certificate request failed." +msgstr "Die Zertifikatsanforderung ist fehlgeschlagen." + +#: includes/class-m365-login-certificate.php:56 +msgid "Certificate signing failed." +msgstr "Das Signieren des Zertifikats ist fehlgeschlagen." + +#: includes/class-m365-login-certificate.php:62 +msgid "Exporting the certificate failed." +msgstr "Der Export des Zertifikats ist fehlgeschlagen." + +#: includes/class-m365-login-certificate.php:83 +msgid "The private key must be in PEM format (-----BEGIN PRIVATE KEY-----)." +msgstr "Der private Schlüssel muss im PEM-Format vorliegen (-----BEGIN PRIVATE KEY-----)." + +#: includes/class-m365-login-certificate.php:86 +msgid "Password protected private keys are not supported. Export the key without a passphrase." +msgstr "Passwortgeschützte private Schlüssel werden nicht unterstützt. Den Schlüssel ohne Passphrase exportieren." + +#: includes/class-m365-login-certificate.php:90 +msgid "The private key could not be read." +msgstr "Der private Schlüssel konnte nicht gelesen werden." + +#: includes/class-m365-login-certificate.php:94 +msgid "Only RSA keys are supported." +msgstr "Es werden nur RSA-Schlüssel unterstützt." + +#: includes/class-m365-login-certificate.php:97 +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 +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 +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 +msgid "The certificate has already expired." +msgstr "Das Zertifikat ist bereits abgelaufen." + +#: includes/class-m365-login-graph.php:201 msgid "Group" msgstr "Gruppe" -#: includes/class-m365-login-graph.php:195 +#: includes/class-m365-login-graph.php:203 msgid "Security group" msgstr "Sicherheitsgruppe" -#: includes/class-m365-login-graph.php:197 +#: includes/class-m365-login-graph.php:205 msgid "Microsoft 365 group" msgstr "Microsoft 365-Gruppe" -#: includes/class-m365-login-settings.php:44 +#: includes/class-m365-login-settings.php:47 msgid "Sign in with Microsoft" msgstr "Login mit Microsoft" -#: includes/class-m365-login-settings.php:53 +#: includes/class-m365-login-settings.php:56 msgid "or" msgstr "oder" -#: includes/class-m365-login-settings.php:272 +#: includes/class-m365-login-settings.php:197 includes/class-m365-login-settings.php:445 +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:391 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:280 +#: includes/class-m365-login-settings.php:399 msgid "The application (client) ID must be a GUID." msgstr "Die Anwendungs-ID (Client) muss eine GUID sein." -#: includes/class-m365-login-settings.php:292 +#: includes/class-m365-login-settings.php:411 msgid "The client secret contains invalid characters." msgstr "Das Client Secret enthält ungültige Zeichen." -#: includes/class-m365-login-settings.php:296 +#: includes/class-m365-login-settings.php:415 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:346 +#: includes/class-m365-login-settings.php:435 +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:437 +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:454 +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:500 msgid "The custom login page must be a URL on this site." msgstr "Die eigene Login-Seite muss eine URL dieser Website sein." diff --git a/languages/m365-login-de_DE_formal.mo b/languages/m365-login-de_DE_formal.mo index d83c951..e5f63fa 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 e16cd6e..2368d77 100644 --- a/languages/m365-login-de_DE_formal.po +++ b/languages/m365-login-de_DE_formal.po @@ -16,511 +16,709 @@ msgstr "" "X-Generator: bin/make-pot.py\n" "X-Domain: m365-login\n" -#: includes/class-m365-login-admin.php:75 includes/class-m365-login-admin.php:76 includes/class-m365-login-admin.php:248 +#: includes/class-m365-login-admin.php:79 includes/class-m365-login-admin.php:80 includes/class-m365-login-admin.php:315 msgid "M365 Login" msgstr "M365 Login" -#: includes/class-m365-login-admin.php:114 +#: includes/class-m365-login-admin.php:118 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:116 +#: includes/class-m365-login-admin.php:120 msgid "Open the settings" msgstr "Einstellungen öffnen" -#: includes/class-m365-login-admin.php:145 +#: includes/class-m365-login-admin.php:150 msgid "Choose button icon" msgstr "Button-Icon auswählen" -#: includes/class-m365-login-admin.php:146 +#: includes/class-m365-login-admin.php:151 msgid "Use this icon" msgstr "Dieses Icon verwenden" -#: includes/class-m365-login-admin.php:147 +#: includes/class-m365-login-admin.php:152 msgid "Copied!" msgstr "Kopiert!" -#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:529 includes/class-m365-login-admin.php:572 +#: includes/class-m365-login-admin.php:153 includes/class-m365-login-admin.php:435 includes/class-m365-login-admin.php:713 includes/class-m365-login-admin.php:756 msgid "Copy" msgstr "Kopieren" -#: includes/class-m365-login-admin.php:149 +#: includes/class-m365-login-admin.php:154 msgid "Testing…" msgstr "Wird geprüft …" -#: includes/class-m365-login-admin.php:150 +#: includes/class-m365-login-admin.php:155 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:151 +#: includes/class-m365-login-admin.php:156 msgid "No groups found." msgstr "Keine Gruppen gefunden." -#: includes/class-m365-login-admin.php:152 +#: includes/class-m365-login-admin.php:157 msgid "Searching…" msgstr "Suche läuft …" -#: includes/class-m365-login-admin.php:153 +#: includes/class-m365-login-admin.php:158 msgid "Add" msgstr "Hinzufügen" -#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:503 +#: includes/class-m365-login-admin.php:159 includes/class-m365-login-admin.php:687 msgid "Remove" msgstr "Entfernen" -#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:488 +#: includes/class-m365-login-admin.php:160 includes/class-m365-login-admin.php:218 includes/class-m365-login-admin.php:672 msgid "Save the connection settings first, then search for groups." msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen." -#: includes/class-m365-login-admin.php:156 +#: includes/class-m365-login-admin.php:161 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:168 includes/class-m365-login-admin.php:207 +#: includes/class-m365-login-admin.php:162 +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:163 +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:164 +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:176 includes/class-m365-login-admin.php:215 includes/class-m365-login-admin.php:241 includes/class-m365-login-admin.php:273 msgid "You are not allowed to do this." msgstr "Dafür fehlt die Berechtigung." -#: includes/class-m365-login-admin.php:173 +#: includes/class-m365-login-admin.php:181 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:187 +#: includes/class-m365-login-admin.php:195 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:196 +#: includes/class-m365-login-admin.php:204 msgid "Tenant reachable. The OpenID configuration was loaded successfully." msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen." -#: includes/class-m365-login-admin.php:219 +#: includes/class-m365-login-admin.php:227 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:232 +#: includes/class-m365-login-admin.php:245 +msgid "Unknown operation." +msgstr "Unbekannte Aktion." + +#: includes/class-m365-login-admin.php:262 +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:279 +msgid "No certificate is stored." +msgstr "Es ist kein Zertifikat gespeichert." + +#: includes/class-m365-login-admin.php:297 msgid "You are not allowed to access this page." msgstr "Für diese Seite fehlt die Berechtigung." -#: includes/class-m365-login-admin.php:249 +#: includes/class-m365-login-admin.php:316 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:254 +#: includes/class-m365-login-admin.php:321 msgid "Connected" msgstr "Verbunden" -#: includes/class-m365-login-admin.php:254 +#: includes/class-m365-login-admin.php:321 msgid "Setup incomplete" msgstr "Einrichtung unvollständig" -#: includes/class-m365-login-admin.php:262 +#: includes/class-m365-login-admin.php:329 msgid "Connection" msgstr "Verbindung" -#: includes/class-m365-login-admin.php:263 +#: includes/class-m365-login-admin.php:330 msgid "Button" msgstr "Button" -#: includes/class-m365-login-admin.php:264 +#: includes/class-m365-login-admin.php:331 msgid "Security" msgstr "Sicherheit" -#: includes/class-m365-login-admin.php:273 +#: includes/class-m365-login-admin.php:340 msgid "Microsoft Entra ID app registration" msgstr "App-Registrierung in Microsoft Entra ID" -#: includes/class-m365-login-admin.php:274 +#: includes/class-m365-login-admin.php:341 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:277 +#: includes/class-m365-login-admin.php:344 msgid "Directory (tenant) ID" msgstr "Verzeichnis-ID (Mandant/Tenant)" -#: includes/class-m365-login-admin.php:280 +#: includes/class-m365-login-admin.php:347 msgid "Test tenant" msgstr "Tenant testen" -#: includes/class-m365-login-admin.php:282 +#: includes/class-m365-login-admin.php:349 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:287 +#: includes/class-m365-login-admin.php:351 +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:357 msgid "Application (client) ID" msgstr "Anwendungs-ID (Client)" -#: includes/class-m365-login-admin.php:292 +#: includes/class-m365-login-admin.php:362 +msgid "How should WordPress authenticate to Microsoft?" +msgstr "Wie soll sich WordPress bei Microsoft authentifizieren?" + +#: includes/class-m365-login-admin.php:367 includes/class-m365-login-admin.php:384 msgid "Client secret" msgstr "Geheimer Clientschlüssel (Client Secret)" -#: includes/class-m365-login-admin.php:294 +#: includes/class-m365-login-admin.php:368 +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:374 +msgid "Certificate" +msgstr "Zertifikat" + +#: includes/class-m365-login-admin.php:374 +msgid "Recommended" +msgstr "Empfohlen" + +#: includes/class-m365-login-admin.php:375 +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:386 msgid "•••••••••••• (stored, leave empty to keep)" msgstr "•••••••••••• (gespeichert – leer lassen, um zu behalten)" -#: includes/class-m365-login-admin.php:294 +#: includes/class-m365-login-admin.php:386 msgid "Paste the secret value" msgstr "Wert des Secrets einfügen" -#: includes/class-m365-login-admin.php:295 +#: includes/class-m365-login-admin.php:387 msgid "Show secret" msgstr "Secret anzeigen" -#: includes/class-m365-login-admin.php:300 +#: includes/class-m365-login-admin.php:392 msgid "Remove the stored secret" msgstr "Gespeichertes Secret entfernen" -#: includes/class-m365-login-admin.php:303 +#: includes/class-m365-login-admin.php:395 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:307 +#: includes/class-m365-login-admin.php:399 +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:402 +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:403 +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:404 +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:405 +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:406 +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:407 +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:409 +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:422 +msgid "Expired" +msgstr "Abgelaufen" + +#. translators: %d: number of days +#: includes/class-m365-login-admin.php:426 +msgid "Expires in %d days" +msgstr "Läuft in %d Tagen ab" + +#. translators: %d: number of days +#: includes/class-m365-login-admin.php:429 +msgid "Valid" +msgstr "Gültig" + +#: includes/class-m365-login-admin.php:434 +msgid "Thumbprint (SHA-1)" +msgstr "Fingerabdruck (SHA-1)" + +#: includes/class-m365-login-admin.php:436 +msgid "Subject" +msgstr "Antragsteller" + +#: includes/class-m365-login-admin.php:438 +msgid "Key size" +msgstr "Schlüssellänge" + +#: includes/class-m365-login-admin.php:440 +msgid "Valid until" +msgstr "Gültig bis" + +#: includes/class-m365-login-admin.php:444 +msgid "Download certificate (.cer)" +msgstr "Zertifikat herunterladen (.cer)" + +#: includes/class-m365-login-admin.php:445 +msgid "Generate new certificate" +msgstr "Neues Zertifikat erzeugen" + +#: includes/class-m365-login-admin.php:448 +msgid "Remove certificate when saving" +msgstr "Zertifikat beim Speichern entfernen" + +#: includes/class-m365-login-admin.php:452 +msgid "No certificate stored yet." +msgstr "Noch kein Zertifikat gespeichert." + +#: includes/class-m365-login-admin.php:454 +msgid "Generate certificate" +msgstr "Zertifikat erzeugen" + +#: includes/class-m365-login-admin.php:455 +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:459 +msgid "Use your own certificate instead (paste PEM)" +msgstr "Stattdessen eigenes Zertifikat verwenden (PEM einfügen)" + +#: includes/class-m365-login-admin.php:462 +msgid "Private key (PEM, unencrypted)" +msgstr "Privater Schlüssel (PEM, unverschlüsselt)" + +#: includes/class-m365-login-admin.php:466 +msgid "Certificate (PEM)" +msgstr "Zertifikat (PEM)" + +#: includes/class-m365-login-admin.php:468 +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:474 +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:477 +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:478 +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:479 +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:480 +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:481 +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:482 +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:484 +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:485 +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:491 msgid "Account prompt" msgstr "Kontoauswahl" -#: includes/class-m365-login-admin.php:309 +#: includes/class-m365-login-admin.php:493 msgid "Always let the user pick an account (recommended)" msgstr "Benutzer wählt immer ein Konto aus (empfohlen)" -#: includes/class-m365-login-admin.php:310 +#: includes/class-m365-login-admin.php:494 msgid "Use the current Microsoft session if available" msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden" -#: includes/class-m365-login-admin.php:311 +#: includes/class-m365-login-admin.php:495 msgid "Always require re-entering credentials" msgstr "Immer erneute Eingabe der Anmeldedaten verlangen" -#: includes/class-m365-login-admin.php:320 +#: includes/class-m365-login-admin.php:504 msgid "Appearance" msgstr "Darstellung" -#: includes/class-m365-login-admin.php:323 +#: includes/class-m365-login-admin.php:507 msgid "Live preview" msgstr "Live-Vorschau" -#: includes/class-m365-login-admin.php:337 +#: includes/class-m365-login-admin.php:521 msgid "Button text" msgstr "Button-Text" -#: includes/class-m365-login-admin.php:341 +#: includes/class-m365-login-admin.php:525 msgid "Divider text" msgstr "Trennlinien-Text" -#: includes/class-m365-login-admin.php:343 +#: includes/class-m365-login-admin.php:527 msgid "Leave empty to hide the divider line." msgstr "Leer lassen, um die Trennlinie auszublenden." -#: includes/class-m365-login-admin.php:348 +#: includes/class-m365-login-admin.php:532 msgid "Icon" msgstr "Icon" -#: includes/class-m365-login-admin.php:351 +#: includes/class-m365-login-admin.php:535 msgid "Show an icon on the button" msgstr "Icon auf dem Button anzeigen" -#: includes/class-m365-login-admin.php:362 +#: includes/class-m365-login-admin.php:546 msgid "Default: Microsoft logo" msgstr "Standard: Microsoft-Logo" -#: includes/class-m365-login-admin.php:364 +#: includes/class-m365-login-admin.php:548 msgid "Choose from media library" msgstr "Aus Mediathek wählen" -#: includes/class-m365-login-admin.php:365 +#: includes/class-m365-login-admin.php:549 msgid "Use Microsoft logo" msgstr "Microsoft-Logo verwenden" -#: includes/class-m365-login-admin.php:367 +#: includes/class-m365-login-admin.php:551 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:375 +#: includes/class-m365-login-admin.php:559 msgid "Background" msgstr "Hintergrund" -#: includes/class-m365-login-admin.php:376 +#: includes/class-m365-login-admin.php:560 msgid "Background (hover)" msgstr "Hintergrund (Hover)" -#: includes/class-m365-login-admin.php:377 +#: includes/class-m365-login-admin.php:561 msgid "Text colour" msgstr "Textfarbe" -#: includes/class-m365-login-admin.php:378 +#: includes/class-m365-login-admin.php:562 msgid "Border" msgstr "Rahmen" -#: includes/class-m365-login-admin.php:391 +#: includes/class-m365-login-admin.php:575 msgid "Corner radius" msgstr "Eckenradius" -#: includes/class-m365-login-admin.php:395 +#: includes/class-m365-login-admin.php:579 msgid "Position on the login page" msgstr "Position auf der Login-Seite" -#: includes/class-m365-login-admin.php:397 +#: includes/class-m365-login-admin.php:581 msgid "Below the login form" msgstr "Unter dem Login-Formular" -#: includes/class-m365-login-admin.php:398 +#: includes/class-m365-login-admin.php:582 msgid "Above the login form" msgstr "Über dem Login-Formular" -#: includes/class-m365-login-admin.php:404 +#: includes/class-m365-login-admin.php:588 msgid "Quick presets" msgstr "Schnellauswahl" -#: includes/class-m365-login-admin.php:405 +#: includes/class-m365-login-admin.php:589 msgid "Microsoft dark" msgstr "Microsoft dunkel" -#: includes/class-m365-login-admin.php:406 +#: includes/class-m365-login-admin.php:590 msgid "Microsoft light" msgstr "Microsoft hell" -#: includes/class-m365-login-admin.php:407 +#: includes/class-m365-login-admin.php:591 msgid "Azure blue" msgstr "Azure-Blau" -#: includes/class-m365-login-admin.php:408 +#: includes/class-m365-login-admin.php:592 msgid "WordPress blue" msgstr "WordPress-Blau" -#: includes/class-m365-login-admin.php:412 +#: includes/class-m365-login-admin.php:596 msgid "Custom login page" msgstr "Eigene Login-Seite" -#: includes/class-m365-login-admin.php:413 +#: includes/class-m365-login-admin.php:597 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:416 +#: includes/class-m365-login-admin.php:600 msgid "URL of your login page" msgstr "URL der Login-Seite" -#: includes/class-m365-login-admin.php:418 +#: includes/class-m365-login-admin.php:602 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:424 +#: includes/class-m365-login-admin.php:608 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:425 +#: includes/class-m365-login-admin.php:609 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:430 +#: includes/class-m365-login-admin.php:614 msgid "Manual placement" msgstr "Manuelle Platzierung" -#: includes/class-m365-login-admin.php:431 +#: includes/class-m365-login-admin.php:615 msgid "Shortcode (block editor, page builders):" msgstr "Shortcode (Block-Editor, Page Builder):" -#: includes/class-m365-login-admin.php:433 +#: includes/class-m365-login-admin.php:617 msgid "Template function (theme files):" msgstr "Template-Funktion (Theme-Dateien):" -#: includes/class-m365-login-admin.php:435 +#: includes/class-m365-login-admin.php:619 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:443 +#: includes/class-m365-login-admin.php:627 msgid "User matching & hardening" msgstr "Benutzerzuordnung & Härtung" -#: includes/class-m365-login-admin.php:444 +#: includes/class-m365-login-admin.php:628 msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists." msgstr "Benutzer werden nie automatisch angelegt. Eine Microsoft-Anmeldung gelingt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert." -#: includes/class-m365-login-admin.php:449 +#: includes/class-m365-login-admin.php:633 msgid "Bind WordPress accounts to the Microsoft object ID" msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden" -#: includes/class-m365-login-admin.php:450 +#: includes/class-m365-login-admin.php:634 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:457 +#: includes/class-m365-login-admin.php:641 msgid "Fall back to the user principal name (UPN)" msgstr "Auf den User Principal Name (UPN) zurückgreifen" -#: includes/class-m365-login-admin.php:458 +#: includes/class-m365-login-admin.php:642 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:465 +#: includes/class-m365-login-admin.php:649 msgid "Keep users signed in (\"Remember me\")" msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)" -#: includes/class-m365-login-admin.php:466 +#: includes/class-m365-login-admin.php:650 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:471 +#: includes/class-m365-login-admin.php:655 msgid "Allowed e-mail domains (optional)" msgstr "Erlaubte E-Mail-Domains (optional)" -#: includes/class-m365-login-admin.php:473 +#: includes/class-m365-login-admin.php:657 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:478 +#: includes/class-m365-login-admin.php:662 msgid "Allowed Entra groups (optional)" msgstr "Erlaubte Entra-Gruppen (optional)" -#: includes/class-m365-login-admin.php:479 +#: includes/class-m365-login-admin.php:663 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:482 +#: includes/class-m365-login-admin.php:666 msgid "Search groups" msgstr "Gruppen suchen" -#: includes/class-m365-login-admin.php:484 +#: includes/class-m365-login-admin.php:668 msgid "Type a group name or paste an object ID…" msgstr "Gruppenname eingeben oder Objekt-ID einfügen …" -#: includes/class-m365-login-admin.php:485 +#: includes/class-m365-login-admin.php:669 msgid "Search" msgstr "Suchen" -#: includes/class-m365-login-admin.php:490 +#: includes/class-m365-login-admin.php:674 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:496 +#: includes/class-m365-login-admin.php:680 msgid "Selected groups" msgstr "Ausgewählte Gruppen" -#: includes/class-m365-login-admin.php:497 +#: includes/class-m365-login-admin.php:681 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:507 +#: includes/class-m365-login-admin.php:691 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:512 +#: includes/class-m365-login-admin.php:696 msgid "Button-only mode" msgstr "Nur-Button-Modus" -#: includes/class-m365-login-admin.php:513 +#: includes/class-m365-login-admin.php:697 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:518 +#: includes/class-m365-login-admin.php:702 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:519 +#: includes/class-m365-login-admin.php:703 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:524 +#: includes/class-m365-login-admin.php:708 msgid "Fallback link (keep it secret)" msgstr "Fallback-Link (geheim halten)" -#: includes/class-m365-login-admin.php:525 +#: includes/class-m365-login-admin.php:709 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:533 +#: includes/class-m365-login-admin.php:717 msgid "Generate a new key when saving" msgstr "Beim Speichern einen neuen Schlüssel erzeugen" -#: includes/class-m365-login-admin.php:536 +#: includes/class-m365-login-admin.php:720 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:542 +#: includes/class-m365-login-admin.php:726 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:551 +#: includes/class-m365-login-admin.php:735 msgid "What the plugin does to keep sign-ins safe" msgstr "So schützt das Plugin die Anmeldung" -#: includes/class-m365-login-admin.php:553 +#: includes/class-m365-login-admin.php:737 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:554 +#: includes/class-m365-login-admin.php:738 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:555 +#: includes/class-m365-login-admin.php:739 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:556 +#: includes/class-m365-login-admin.php:740 msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed." msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert." -#: includes/class-m365-login-admin.php:562 +#: includes/class-m365-login-admin.php:746 msgid "Save changes" msgstr "Änderungen speichern" -#: includes/class-m365-login-admin.php:568 +#: includes/class-m365-login-admin.php:752 msgid "Redirect URI" msgstr "Umleitungs-URI (Redirect URI)" -#: includes/class-m365-login-admin.php:569 +#: includes/class-m365-login-admin.php:753 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:575 +#: includes/class-m365-login-admin.php:759 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:578 +#: includes/class-m365-login-admin.php:762 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:583 -msgid "Setup in 5 steps" -msgstr "Einrichtung in 5 Schritten" +#: includes/class-m365-login-admin.php:767 +msgid "Setup guide: app registration" +msgstr "Anleitung: App-Registrierung" -#: includes/class-m365-login-admin.php:585 -msgid "Open the Microsoft Entra admin center → App registrations → New registration." -msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung." +#: includes/class-m365-login-admin.php:769 +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:586 -msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above." -msgstr "„Nur Konten in diesem Organisationsverzeichnis“ wählen, Plattform „Web“ auswählen und die Umleitungs-URI von oben einfügen." +#: includes/class-m365-login-admin.php:770 +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:587 -msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page." -msgstr "Anwendungs-ID (Client) und Verzeichnis-ID (Mandant) von der Übersichtsseite kopieren." +#: includes/class-m365-login-admin.php:771 +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:588 -msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)." -msgstr "Unter „Zertifikate & Geheimnisse“ einen geheimen Clientschlüssel erstellen und dessen Wert (nicht die ID) kopieren." +#: includes/class-m365-login-admin.php:772 +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:589 -msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page." -msgstr "Unter „Tokenkonfiguration“ den optionalen Anspruch „email“ für ID-Tokens hinzufügen (empfohlen), dann diese Seite speichern." +#: includes/class-m365-login-admin.php:773 +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:591 +#: includes/class-m365-login-admin.php:774 +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:775 +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:776 +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:778 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:592 +#: includes/class-m365-login-admin.php:779 msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent." msgstr "Optional für Gruppen-Beschränkungen: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung." -#: includes/class-m365-login-admin.php:596 +#: includes/class-m365-login-admin.php:783 msgid "Shortcode" msgstr "Shortcode" -#: includes/class-m365-login-admin.php:597 +#: includes/class-m365-login-admin.php:784 msgid "Place the button on a custom login page:" msgstr "Button auf einer eigenen Login-Seite platzieren:" -#: includes/class-m365-login-admin.php:599 +#: includes/class-m365-login-admin.php:786 msgid "More options on the Button tab under \"Custom login page\"." msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“." @@ -528,107 +726,179 @@ msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“." 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:777 +#: includes/class-m365-login-auth.php:823 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:798 includes/class-m365-login-graph.php:63 +#: includes/class-m365-login-auth.php:844 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:799 +#: includes/class-m365-login-auth.php:845 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:800 +#: includes/class-m365-login-auth.php:846 msgid "Microsoft sign-in was cancelled." msgstr "Die Microsoft-Anmeldung wurde abgebrochen." -#: includes/class-m365-login-auth.php:801 +#: includes/class-m365-login-auth.php:847 msgid "Microsoft returned an error. Please try again." msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen." -#: includes/class-m365-login-auth.php:802 +#: includes/class-m365-login-auth.php:848 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:803 +#: includes/class-m365-login-auth.php:849 msgid "The Microsoft sign-in could not be verified." msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden." -#: includes/class-m365-login-auth.php:804 +#: includes/class-m365-login-auth.php:850 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:805 +#: includes/class-m365-login-auth.php:851 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:806 +#: includes/class-m365-login-auth.php:852 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:807 +#: includes/class-m365-login-auth.php:853 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:808 +#: includes/class-m365-login-auth.php:854 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:809 +#: includes/class-m365-login-auth.php:855 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:810 +#: includes/class-m365-login-auth.php:856 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:811 +#: includes/class-m365-login-auth.php:857 msgid "The fallback key is not valid." msgstr "Der Fallback-Schlüssel ist ungültig." -#: includes/class-m365-login-auth.php:812 +#: includes/class-m365-login-auth.php:858 msgid "Too many attempts. Please wait 15 minutes." msgstr "Zu viele Versuche. Bitte 15 Minuten warten." -#: includes/class-m365-login-graph.php:193 +#: includes/class-m365-login-auth.php:859 +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-certificate.php:29 +msgid "The PHP OpenSSL extension is not available." +msgstr "Die PHP-Erweiterung OpenSSL ist nicht verfügbar." + +#: includes/class-m365-login-certificate.php:42 +msgid "Key generation failed." +msgstr "Die Schlüsselerzeugung ist fehlgeschlagen." + +#: includes/class-m365-login-certificate.php:51 +msgid "Certificate request failed." +msgstr "Die Zertifikatsanforderung ist fehlgeschlagen." + +#: includes/class-m365-login-certificate.php:56 +msgid "Certificate signing failed." +msgstr "Das Signieren des Zertifikats ist fehlgeschlagen." + +#: includes/class-m365-login-certificate.php:62 +msgid "Exporting the certificate failed." +msgstr "Der Export des Zertifikats ist fehlgeschlagen." + +#: includes/class-m365-login-certificate.php:83 +msgid "The private key must be in PEM format (-----BEGIN PRIVATE KEY-----)." +msgstr "Der private Schlüssel muss im PEM-Format vorliegen (-----BEGIN PRIVATE KEY-----)." + +#: includes/class-m365-login-certificate.php:86 +msgid "Password protected private keys are not supported. Export the key without a passphrase." +msgstr "Passwortgeschützte private Schlüssel werden nicht unterstützt. Den Schlüssel ohne Passphrase exportieren." + +#: includes/class-m365-login-certificate.php:90 +msgid "The private key could not be read." +msgstr "Der private Schlüssel konnte nicht gelesen werden." + +#: includes/class-m365-login-certificate.php:94 +msgid "Only RSA keys are supported." +msgstr "Es werden nur RSA-Schlüssel unterstützt." + +#: includes/class-m365-login-certificate.php:97 +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 +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 +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 +msgid "The certificate has already expired." +msgstr "Das Zertifikat ist bereits abgelaufen." + +#: includes/class-m365-login-graph.php:201 msgid "Group" msgstr "Gruppe" -#: includes/class-m365-login-graph.php:195 +#: includes/class-m365-login-graph.php:203 msgid "Security group" msgstr "Sicherheitsgruppe" -#: includes/class-m365-login-graph.php:197 +#: includes/class-m365-login-graph.php:205 msgid "Microsoft 365 group" msgstr "Microsoft 365-Gruppe" -#: includes/class-m365-login-settings.php:44 +#: includes/class-m365-login-settings.php:47 msgid "Sign in with Microsoft" msgstr "Login mit Microsoft" -#: includes/class-m365-login-settings.php:53 +#: includes/class-m365-login-settings.php:56 msgid "or" msgstr "oder" -#: includes/class-m365-login-settings.php:272 +#: includes/class-m365-login-settings.php:197 includes/class-m365-login-settings.php:445 +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:391 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:280 +#: includes/class-m365-login-settings.php:399 msgid "The application (client) ID must be a GUID." msgstr "Die Anwendungs-ID (Client) muss eine GUID sein." -#: includes/class-m365-login-settings.php:292 +#: includes/class-m365-login-settings.php:411 msgid "The client secret contains invalid characters." msgstr "Das Client Secret enthält ungültige Zeichen." -#: includes/class-m365-login-settings.php:296 +#: includes/class-m365-login-settings.php:415 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:346 +#: includes/class-m365-login-settings.php:435 +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:437 +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:454 +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:500 msgid "The custom login page must be a URL on this site." msgstr "Die eigene Login-Seite muss eine URL dieser Website sein." diff --git a/languages/m365-login.pot b/languages/m365-login.pot index 512e6a3..0e6969e 100644 --- a/languages/m365-login.pot +++ b/languages/m365-login.pot @@ -14,511 +14,709 @@ msgstr "" "X-Generator: bin/make-pot.py\n" "X-Domain: m365-login\n" -#: includes/class-m365-login-admin.php:75 includes/class-m365-login-admin.php:76 includes/class-m365-login-admin.php:248 +#: includes/class-m365-login-admin.php:79 includes/class-m365-login-admin.php:80 includes/class-m365-login-admin.php:315 msgid "M365 Login" msgstr "" -#: includes/class-m365-login-admin.php:114 +#: includes/class-m365-login-admin.php:118 msgid "M365 Login is active but not connected to Microsoft Entra ID yet." msgstr "" -#: includes/class-m365-login-admin.php:116 +#: includes/class-m365-login-admin.php:120 msgid "Open the settings" msgstr "" -#: includes/class-m365-login-admin.php:145 +#: includes/class-m365-login-admin.php:150 msgid "Choose button icon" msgstr "" -#: includes/class-m365-login-admin.php:146 +#: includes/class-m365-login-admin.php:151 msgid "Use this icon" msgstr "" -#: includes/class-m365-login-admin.php:147 +#: includes/class-m365-login-admin.php:152 msgid "Copied!" msgstr "" -#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:529 includes/class-m365-login-admin.php:572 +#: includes/class-m365-login-admin.php:153 includes/class-m365-login-admin.php:435 includes/class-m365-login-admin.php:713 includes/class-m365-login-admin.php:756 msgid "Copy" msgstr "" -#: includes/class-m365-login-admin.php:149 +#: includes/class-m365-login-admin.php:154 msgid "Testing…" msgstr "" -#: includes/class-m365-login-admin.php:150 +#: includes/class-m365-login-admin.php:155 msgid "The tenant could not be reached. Check the tenant ID and the server’s outgoing connections." msgstr "" -#: includes/class-m365-login-admin.php:151 +#: includes/class-m365-login-admin.php:156 msgid "No groups found." msgstr "" -#: includes/class-m365-login-admin.php:152 +#: includes/class-m365-login-admin.php:157 msgid "Searching…" msgstr "" -#: includes/class-m365-login-admin.php:153 +#: includes/class-m365-login-admin.php:158 msgid "Add" msgstr "" -#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:503 +#: includes/class-m365-login-admin.php:159 includes/class-m365-login-admin.php:687 msgid "Remove" msgstr "" -#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:488 +#: includes/class-m365-login-admin.php:160 includes/class-m365-login-admin.php:218 includes/class-m365-login-admin.php:672 msgid "Save the connection settings first, then search for groups." msgstr "" -#: includes/class-m365-login-admin.php:156 +#: includes/class-m365-login-admin.php:161 msgid "Generate a new fallback key on save? The old link stops working." msgstr "" -#: includes/class-m365-login-admin.php:168 includes/class-m365-login-admin.php:207 +#: includes/class-m365-login-admin.php:162 +msgid "Generating a 3072-bit key pair, this takes a moment…" +msgstr "" + +#: includes/class-m365-login-admin.php:163 +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:164 +msgid "Remove the stored certificate when saving? Sign-in with the certificate method stops working." +msgstr "" + +#: includes/class-m365-login-admin.php:176 includes/class-m365-login-admin.php:215 includes/class-m365-login-admin.php:241 includes/class-m365-login-admin.php:273 msgid "You are not allowed to do this." msgstr "" -#: includes/class-m365-login-admin.php:173 +#: includes/class-m365-login-admin.php:181 msgid "Please enter a valid tenant ID first." msgstr "" #. translators: %d: HTTP status code -#: includes/class-m365-login-admin.php:187 +#: includes/class-m365-login-admin.php:195 msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?" msgstr "" #. translators: %d: HTTP status code -#: includes/class-m365-login-admin.php:196 +#: includes/class-m365-login-admin.php:204 msgid "Tenant reachable. The OpenID configuration was loaded successfully." msgstr "" -#: includes/class-m365-login-admin.php:219 +#: includes/class-m365-login-admin.php:227 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:232 -msgid "You are not allowed to access this page." -msgstr "" - -#: includes/class-m365-login-admin.php:249 -msgid "Let existing users sign in with their Microsoft 365 / Entra ID account." -msgstr "" - -#: includes/class-m365-login-admin.php:254 -msgid "Connected" -msgstr "" - -#: includes/class-m365-login-admin.php:254 -msgid "Setup incomplete" +#: includes/class-m365-login-admin.php:245 +msgid "Unknown operation." msgstr "" #: includes/class-m365-login-admin.php:262 +msgid "Certificate generated and stored. Download the .cer file and upload it in Entra ID." +msgstr "" + +#: includes/class-m365-login-admin.php:279 +msgid "No certificate is stored." +msgstr "" + +#: includes/class-m365-login-admin.php:297 +msgid "You are not allowed to access this page." +msgstr "" + +#: includes/class-m365-login-admin.php:316 +msgid "Let existing users sign in with their Microsoft 365 / Entra ID account." +msgstr "" + +#: includes/class-m365-login-admin.php:321 +msgid "Connected" +msgstr "" + +#: includes/class-m365-login-admin.php:321 +msgid "Setup incomplete" +msgstr "" + +#: includes/class-m365-login-admin.php:329 msgid "Connection" msgstr "" -#: includes/class-m365-login-admin.php:263 +#: includes/class-m365-login-admin.php:330 msgid "Button" msgstr "" -#: includes/class-m365-login-admin.php:264 +#: includes/class-m365-login-admin.php:331 msgid "Security" msgstr "" -#: includes/class-m365-login-admin.php:273 +#: includes/class-m365-login-admin.php:340 msgid "Microsoft Entra ID app registration" msgstr "" -#: includes/class-m365-login-admin.php:274 +#: includes/class-m365-login-admin.php:341 msgid "Enter the values from your app registration in the Microsoft Entra admin center." msgstr "" -#: includes/class-m365-login-admin.php:277 +#: includes/class-m365-login-admin.php:344 msgid "Directory (tenant) ID" msgstr "" -#: includes/class-m365-login-admin.php:280 +#: includes/class-m365-login-admin.php:347 msgid "Test tenant" msgstr "" -#: includes/class-m365-login-admin.php:282 +#: includes/class-m365-login-admin.php:349 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:287 +#: includes/class-m365-login-admin.php:351 +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:357 msgid "Application (client) ID" msgstr "" -#: includes/class-m365-login-admin.php:292 +#: includes/class-m365-login-admin.php:362 +msgid "How should WordPress authenticate to Microsoft?" +msgstr "" + +#: includes/class-m365-login-admin.php:367 includes/class-m365-login-admin.php:384 msgid "Client secret" msgstr "" -#: includes/class-m365-login-admin.php:294 -msgid "•••••••••••• (stored, leave empty to keep)" +#: includes/class-m365-login-admin.php:368 +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:294 -msgid "Paste the secret value" +#: includes/class-m365-login-admin.php:374 +msgid "Certificate" msgstr "" -#: includes/class-m365-login-admin.php:295 -msgid "Show secret" -msgstr "" - -#: includes/class-m365-login-admin.php:300 -msgid "Remove the stored secret" -msgstr "" - -#: includes/class-m365-login-admin.php:303 -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:307 -msgid "Account prompt" -msgstr "" - -#: includes/class-m365-login-admin.php:309 -msgid "Always let the user pick an account (recommended)" -msgstr "" - -#: includes/class-m365-login-admin.php:310 -msgid "Use the current Microsoft session if available" -msgstr "" - -#: includes/class-m365-login-admin.php:311 -msgid "Always require re-entering credentials" -msgstr "" - -#: includes/class-m365-login-admin.php:320 -msgid "Appearance" -msgstr "" - -#: includes/class-m365-login-admin.php:323 -msgid "Live preview" -msgstr "" - -#: includes/class-m365-login-admin.php:337 -msgid "Button text" -msgstr "" - -#: includes/class-m365-login-admin.php:341 -msgid "Divider text" -msgstr "" - -#: includes/class-m365-login-admin.php:343 -msgid "Leave empty to hide the divider line." -msgstr "" - -#: includes/class-m365-login-admin.php:348 -msgid "Icon" -msgstr "" - -#: includes/class-m365-login-admin.php:351 -msgid "Show an icon on the button" -msgstr "" - -#: includes/class-m365-login-admin.php:362 -msgid "Default: Microsoft logo" -msgstr "" - -#: includes/class-m365-login-admin.php:364 -msgid "Choose from media library" -msgstr "" - -#: includes/class-m365-login-admin.php:365 -msgid "Use Microsoft logo" -msgstr "" - -#: includes/class-m365-login-admin.php:367 -msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best." +#: includes/class-m365-login-admin.php:374 +msgid "Recommended" msgstr "" #: includes/class-m365-login-admin.php:375 -msgid "Background" +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:376 -msgid "Background (hover)" +#: includes/class-m365-login-admin.php:386 +msgid "•••••••••••• (stored, leave empty to keep)" msgstr "" -#: includes/class-m365-login-admin.php:377 -msgid "Text colour" +#: includes/class-m365-login-admin.php:386 +msgid "Paste the secret value" msgstr "" -#: includes/class-m365-login-admin.php:378 -msgid "Border" +#: includes/class-m365-login-admin.php:387 +msgid "Show secret" msgstr "" -#: includes/class-m365-login-admin.php:391 -msgid "Corner radius" +#: includes/class-m365-login-admin.php:392 +msgid "Remove the stored secret" msgstr "" #: includes/class-m365-login-admin.php:395 -msgid "Position on the login page" +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:397 -msgid "Below the login form" +#: includes/class-m365-login-admin.php:399 +msgid "Step-by-step: create a client secret in Entra ID" msgstr "" -#: includes/class-m365-login-admin.php:398 -msgid "Above the login form" +#: includes/class-m365-login-admin.php:402 +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:403 +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:404 -msgid "Quick presets" +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:405 -msgid "Microsoft dark" +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:406 -msgid "Microsoft light" +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:407 -msgid "Azure blue" +msgid "Paste the value into the Client secret field above and save this page." msgstr "" -#: includes/class-m365-login-admin.php:408 -msgid "WordPress blue" +#: includes/class-m365-login-admin.php:409 +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:412 -msgid "Custom login page" +#: includes/class-m365-login-admin.php:422 +msgid "Expired" msgstr "" -#: includes/class-m365-login-admin.php:413 -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." +#. translators: %d: number of days +#: includes/class-m365-login-admin.php:426 +msgid "Expires in %d days" msgstr "" -#: includes/class-m365-login-admin.php:416 -msgid "URL of your login page" +#. translators: %d: number of days +#: includes/class-m365-login-admin.php:429 +msgid "Valid" msgstr "" -#: includes/class-m365-login-admin.php:418 -msgid "Must be on this site. Leave empty to use wp-login.php." +#: includes/class-m365-login-admin.php:434 +msgid "Thumbprint (SHA-1)" msgstr "" -#: includes/class-m365-login-admin.php:424 -msgid "Add the button to every wp_login_form() form automatically" +#: includes/class-m365-login-admin.php:436 +msgid "Subject" msgstr "" -#: includes/class-m365-login-admin.php:425 -msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below." +#: includes/class-m365-login-admin.php:438 +msgid "Key size" msgstr "" -#: includes/class-m365-login-admin.php:430 -msgid "Manual placement" -msgstr "" - -#: includes/class-m365-login-admin.php:431 -msgid "Shortcode (block editor, page builders):" -msgstr "" - -#: includes/class-m365-login-admin.php:433 -msgid "Template function (theme files):" -msgstr "" - -#: includes/class-m365-login-admin.php:435 -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:443 -msgid "User matching & hardening" +#: includes/class-m365-login-admin.php:440 +msgid "Valid until" msgstr "" #: includes/class-m365-login-admin.php:444 -msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists." +msgid "Download certificate (.cer)" msgstr "" -#: includes/class-m365-login-admin.php:449 -msgid "Bind WordPress accounts to the Microsoft object ID" +#: includes/class-m365-login-admin.php:445 +msgid "Generate new certificate" msgstr "" -#: includes/class-m365-login-admin.php:450 -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." +#: includes/class-m365-login-admin.php:448 +msgid "Remove certificate when saving" msgstr "" -#: includes/class-m365-login-admin.php:457 -msgid "Fall back to the user principal name (UPN)" +#: includes/class-m365-login-admin.php:452 +msgid "No certificate stored yet." msgstr "" -#: includes/class-m365-login-admin.php:458 -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." +#: includes/class-m365-login-admin.php:454 +msgid "Generate certificate" msgstr "" -#: includes/class-m365-login-admin.php:465 -msgid "Keep users signed in (\"Remember me\")" +#: includes/class-m365-login-admin.php:455 +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:459 +msgid "Use your own certificate instead (paste PEM)" +msgstr "" + +#: includes/class-m365-login-admin.php:462 +msgid "Private key (PEM, unencrypted)" msgstr "" #: includes/class-m365-login-admin.php:466 -msgid "Issues a 14-day WordPress session instead of a browser session." +msgid "Certificate (PEM)" msgstr "" -#: includes/class-m365-login-admin.php:471 -msgid "Allowed e-mail domains (optional)" +#: includes/class-m365-login-admin.php:468 +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:473 -msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant." +#: includes/class-m365-login-admin.php:474 +msgid "Step-by-step: register the certificate in Entra ID" +msgstr "" + +#: includes/class-m365-login-admin.php:477 +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:478 -msgid "Allowed Entra groups (optional)" +msgid "Open entra.microsoft.com → Identity → Applications → App registrations and open your app." msgstr "" #: includes/class-m365-login-admin.php:479 -msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count." +msgid "Choose Certificates & secrets in the left menu, then the tab Certificates, and click Upload certificate." +msgstr "" + +#: includes/class-m365-login-admin.php:480 +msgid "Select the downloaded .cer file, add a description such as \"WordPress login\" and click Add." +msgstr "" + +#: includes/class-m365-login-admin.php:481 +msgid "Compare the thumbprint Entra ID shows with the thumbprint above – they must match exactly." msgstr "" #: includes/class-m365-login-admin.php:482 -msgid "Search groups" +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:484 -msgid "Type a group name or paste an object ID…" +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:485 -msgid "Search" +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:490 -msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs." +#: includes/class-m365-login-admin.php:491 +msgid "Account prompt" msgstr "" -#: includes/class-m365-login-admin.php:496 -msgid "Selected groups" +#: includes/class-m365-login-admin.php:493 +msgid "Always let the user pick an account (recommended)" msgstr "" -#: includes/class-m365-login-admin.php:497 -msgid "No groups selected – every matched user may sign in." +#: includes/class-m365-login-admin.php:494 +msgid "Use the current Microsoft session if available" +msgstr "" + +#: includes/class-m365-login-admin.php:495 +msgid "Always require re-entering credentials" +msgstr "" + +#: includes/class-m365-login-admin.php:504 +msgid "Appearance" msgstr "" #: includes/class-m365-login-admin.php:507 -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." +msgid "Live preview" msgstr "" -#: includes/class-m365-login-admin.php:512 -msgid "Button-only mode" -msgstr "" - -#: includes/class-m365-login-admin.php:513 -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 "" - -#: includes/class-m365-login-admin.php:518 -msgid "Show only the Microsoft button on the login page" -msgstr "" - -#: includes/class-m365-login-admin.php:519 -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:524 -msgid "Fallback link (keep it secret)" +#: includes/class-m365-login-admin.php:521 +msgid "Button text" msgstr "" #: includes/class-m365-login-admin.php:525 +msgid "Divider text" +msgstr "" + +#: includes/class-m365-login-admin.php:527 +msgid "Leave empty to hide the divider line." +msgstr "" + +#: includes/class-m365-login-admin.php:532 +msgid "Icon" +msgstr "" + +#: includes/class-m365-login-admin.php:535 +msgid "Show an icon on the button" +msgstr "" + +#: includes/class-m365-login-admin.php:546 +msgid "Default: Microsoft logo" +msgstr "" + +#: includes/class-m365-login-admin.php:548 +msgid "Choose from media library" +msgstr "" + +#: includes/class-m365-login-admin.php:549 +msgid "Use Microsoft logo" +msgstr "" + +#: includes/class-m365-login-admin.php:551 +msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best." +msgstr "" + +#: includes/class-m365-login-admin.php:559 +msgid "Background" +msgstr "" + +#: includes/class-m365-login-admin.php:560 +msgid "Background (hover)" +msgstr "" + +#: includes/class-m365-login-admin.php:561 +msgid "Text colour" +msgstr "" + +#: includes/class-m365-login-admin.php:562 +msgid "Border" +msgstr "" + +#: includes/class-m365-login-admin.php:575 +msgid "Corner radius" +msgstr "" + +#: includes/class-m365-login-admin.php:579 +msgid "Position on the login page" +msgstr "" + +#: includes/class-m365-login-admin.php:581 +msgid "Below the login form" +msgstr "" + +#: includes/class-m365-login-admin.php:582 +msgid "Above the login form" +msgstr "" + +#: includes/class-m365-login-admin.php:588 +msgid "Quick presets" +msgstr "" + +#: includes/class-m365-login-admin.php:589 +msgid "Microsoft dark" +msgstr "" + +#: includes/class-m365-login-admin.php:590 +msgid "Microsoft light" +msgstr "" + +#: includes/class-m365-login-admin.php:591 +msgid "Azure blue" +msgstr "" + +#: includes/class-m365-login-admin.php:592 +msgid "WordPress blue" +msgstr "" + +#: includes/class-m365-login-admin.php:596 +msgid "Custom login page" +msgstr "" + +#: includes/class-m365-login-admin.php:597 +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:600 +msgid "URL of your login page" +msgstr "" + +#: includes/class-m365-login-admin.php:602 +msgid "Must be on this site. Leave empty to use wp-login.php." +msgstr "" + +#: includes/class-m365-login-admin.php:608 +msgid "Add the button to every wp_login_form() form automatically" +msgstr "" + +#: includes/class-m365-login-admin.php:609 +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:614 +msgid "Manual placement" +msgstr "" + +#: includes/class-m365-login-admin.php:615 +msgid "Shortcode (block editor, page builders):" +msgstr "" + +#: includes/class-m365-login-admin.php:617 +msgid "Template function (theme files):" +msgstr "" + +#: includes/class-m365-login-admin.php:619 +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:627 +msgid "User matching & hardening" +msgstr "" + +#: includes/class-m365-login-admin.php:628 +msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists." +msgstr "" + +#: includes/class-m365-login-admin.php:633 +msgid "Bind WordPress accounts to the Microsoft object ID" +msgstr "" + +#: includes/class-m365-login-admin.php:634 +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:641 +msgid "Fall back to the user principal name (UPN)" +msgstr "" + +#: includes/class-m365-login-admin.php:642 +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:649 +msgid "Keep users signed in (\"Remember me\")" +msgstr "" + +#: includes/class-m365-login-admin.php:650 +msgid "Issues a 14-day WordPress session instead of a browser session." +msgstr "" + +#: includes/class-m365-login-admin.php:655 +msgid "Allowed e-mail domains (optional)" +msgstr "" + +#: includes/class-m365-login-admin.php:657 +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:662 +msgid "Allowed Entra groups (optional)" +msgstr "" + +#: includes/class-m365-login-admin.php:663 +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:666 +msgid "Search groups" +msgstr "" + +#: includes/class-m365-login-admin.php:668 +msgid "Type a group name or paste an object ID…" +msgstr "" + +#: includes/class-m365-login-admin.php:669 +msgid "Search" +msgstr "" + +#: includes/class-m365-login-admin.php:674 +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:680 +msgid "Selected groups" +msgstr "" + +#: includes/class-m365-login-admin.php:681 +msgid "No groups selected – every matched user may sign in." +msgstr "" + +#: includes/class-m365-login-admin.php:691 +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:696 +msgid "Button-only mode" +msgstr "" + +#: includes/class-m365-login-admin.php:697 +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 "" + +#: includes/class-m365-login-admin.php:702 +msgid "Show only the Microsoft button on the login page" +msgstr "" + +#: includes/class-m365-login-admin.php:703 +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:708 +msgid "Fallback link (keep it secret)" +msgstr "" + +#: includes/class-m365-login-admin.php:709 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:533 +#: includes/class-m365-login-admin.php:717 msgid "Generate a new key when saving" msgstr "" -#: includes/class-m365-login-admin.php:536 +#: includes/class-m365-login-admin.php:720 msgid "A key is generated automatically the first time you save these settings." msgstr "" #. translators: %s: PHP constant -#: includes/class-m365-login-admin.php:542 +#: includes/class-m365-login-admin.php:726 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:551 +#: includes/class-m365-login-admin.php:735 msgid "What the plugin does to keep sign-ins safe" msgstr "" -#: includes/class-m365-login-admin.php:553 +#: includes/class-m365-login-admin.php:737 msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser." msgstr "" -#: includes/class-m365-login-admin.php:554 +#: includes/class-m365-login-admin.php:738 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:555 +#: includes/class-m365-login-admin.php:739 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:556 +#: includes/class-m365-login-admin.php:740 msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed." msgstr "" -#: includes/class-m365-login-admin.php:562 +#: includes/class-m365-login-admin.php:746 msgid "Save changes" msgstr "" -#: includes/class-m365-login-admin.php:568 +#: includes/class-m365-login-admin.php:752 msgid "Redirect URI" msgstr "" -#: includes/class-m365-login-admin.php:569 +#: includes/class-m365-login-admin.php:753 msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:" msgstr "" -#: includes/class-m365-login-admin.php:575 +#: includes/class-m365-login-admin.php:759 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:578 +#: includes/class-m365-login-admin.php:762 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:583 -msgid "Setup in 5 steps" +#: includes/class-m365-login-admin.php:767 +msgid "Setup guide: app registration" msgstr "" -#: includes/class-m365-login-admin.php:585 -msgid "Open the Microsoft Entra admin center → App registrations → New registration." +#: includes/class-m365-login-admin.php:769 +msgid "Open entra.microsoft.com → Identity → Applications → App registrations → New registration." msgstr "" -#: includes/class-m365-login-admin.php:586 -msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above." +#: includes/class-m365-login-admin.php:770 +msgid "Name: e.g. \"WordPress login\". Supported account types: \"Accounts in this organizational directory only\" (single tenant)." msgstr "" -#: includes/class-m365-login-admin.php:587 -msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page." +#: includes/class-m365-login-admin.php:771 +msgid "Redirect URI: choose the platform Web and paste the URI shown above. Then click Register." msgstr "" -#: includes/class-m365-login-admin.php:588 -msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)." +#: includes/class-m365-login-admin.php:772 +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:589 -msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page." +#: includes/class-m365-login-admin.php:773 +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:591 +#: includes/class-m365-login-admin.php:774 +msgid "Token configuration → Add optional claim → ID → tick \"email\" → Add. Confirm the API permission prompt." +msgstr "" + +#: includes/class-m365-login-admin.php:775 +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:776 +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:778 msgid "Required API permission: openid, profile, email (delegated) – granted by default." msgstr "" -#: includes/class-m365-login-admin.php:592 +#: includes/class-m365-login-admin.php:779 msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent." msgstr "" -#: includes/class-m365-login-admin.php:596 +#: includes/class-m365-login-admin.php:783 msgid "Shortcode" msgstr "" -#: includes/class-m365-login-admin.php:597 +#: includes/class-m365-login-admin.php:784 msgid "Place the button on a custom login page:" msgstr "" -#: includes/class-m365-login-admin.php:599 +#: includes/class-m365-login-admin.php:786 msgid "More options on the Button tab under \"Custom login page\"." msgstr "" @@ -526,107 +724,179 @@ msgstr "" msgid "Password sign-in is disabled on this site. Please use the Microsoft button." msgstr "" -#: includes/class-m365-login-auth.php:777 +#: includes/class-m365-login-auth.php:823 msgid "Password sign-in is temporarily enabled for this browser (30 minutes)." msgstr "" -#: includes/class-m365-login-auth.php:798 includes/class-m365-login-graph.php:63 +#: includes/class-m365-login-auth.php:844 includes/class-m365-login-graph.php:63 msgid "Microsoft login is not configured yet." msgstr "" -#: includes/class-m365-login-auth.php:799 +#: includes/class-m365-login-auth.php:845 msgid "The login request expired or was invalid. Please try again." msgstr "" -#: includes/class-m365-login-auth.php:800 +#: includes/class-m365-login-auth.php:846 msgid "Microsoft sign-in was cancelled." msgstr "" -#: includes/class-m365-login-auth.php:801 +#: includes/class-m365-login-auth.php:847 msgid "Microsoft returned an error. Please try again." msgstr "" -#: includes/class-m365-login-auth.php:802 +#: includes/class-m365-login-auth.php:848 msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator." msgstr "" -#: includes/class-m365-login-auth.php:803 +#: includes/class-m365-login-auth.php:849 msgid "The Microsoft sign-in could not be verified." msgstr "" -#: includes/class-m365-login-auth.php:804 +#: includes/class-m365-login-auth.php:850 msgid "Your Microsoft account did not provide an e-mail address." msgstr "" -#: includes/class-m365-login-auth.php:805 +#: includes/class-m365-login-auth.php:851 msgid "Your e-mail domain is not allowed to sign in here." msgstr "" -#: includes/class-m365-login-auth.php:806 +#: includes/class-m365-login-auth.php:852 msgid "No WordPress account exists for your Microsoft e-mail address." msgstr "" -#: includes/class-m365-login-auth.php:807 +#: includes/class-m365-login-auth.php:853 msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator." msgstr "" -#: includes/class-m365-login-auth.php:808 +#: includes/class-m365-login-auth.php:854 msgid "You are not allowed to sign in with this account." msgstr "" -#: includes/class-m365-login-auth.php:809 +#: includes/class-m365-login-auth.php:855 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:810 +#: includes/class-m365-login-auth.php:856 msgid "Your group membership could not be verified. Please contact an administrator." msgstr "" -#: includes/class-m365-login-auth.php:811 +#: includes/class-m365-login-auth.php:857 msgid "The fallback key is not valid." msgstr "" -#: includes/class-m365-login-auth.php:812 +#: includes/class-m365-login-auth.php:858 msgid "Too many attempts. Please wait 15 minutes." msgstr "" -#: includes/class-m365-login-graph.php:193 +#: includes/class-m365-login-auth.php:859 +msgid "Too many sign-in attempts from your connection. Please wait a few minutes and try again." +msgstr "" + +#: includes/class-m365-login-certificate.php:29 +msgid "The PHP OpenSSL extension is not available." +msgstr "" + +#: includes/class-m365-login-certificate.php:42 +msgid "Key generation failed." +msgstr "" + +#: includes/class-m365-login-certificate.php:51 +msgid "Certificate request failed." +msgstr "" + +#: includes/class-m365-login-certificate.php:56 +msgid "Certificate signing failed." +msgstr "" + +#: includes/class-m365-login-certificate.php:62 +msgid "Exporting the certificate failed." +msgstr "" + +#: includes/class-m365-login-certificate.php:83 +msgid "The private key must be in PEM format (-----BEGIN PRIVATE KEY-----)." +msgstr "" + +#: includes/class-m365-login-certificate.php:86 +msgid "Password protected private keys are not supported. Export the key without a passphrase." +msgstr "" + +#: includes/class-m365-login-certificate.php:90 +msgid "The private key could not be read." +msgstr "" + +#: includes/class-m365-login-certificate.php:94 +msgid "Only RSA keys are supported." +msgstr "" + +#: includes/class-m365-login-certificate.php:97 +msgid "The RSA key must have at least 2048 bits." +msgstr "" + +#: includes/class-m365-login-certificate.php:102 +msgid "The certificate could not be read. Paste it in PEM format (-----BEGIN CERTIFICATE-----)." +msgstr "" + +#: includes/class-m365-login-certificate.php:105 +msgid "The certificate does not belong to this private key." +msgstr "" + +#: includes/class-m365-login-certificate.php:110 +msgid "The certificate has already expired." +msgstr "" + +#: includes/class-m365-login-graph.php:201 msgid "Group" msgstr "" -#: includes/class-m365-login-graph.php:195 +#: includes/class-m365-login-graph.php:203 msgid "Security group" msgstr "" -#: includes/class-m365-login-graph.php:197 +#: includes/class-m365-login-graph.php:205 msgid "Microsoft 365 group" msgstr "" -#: includes/class-m365-login-settings.php:44 +#: includes/class-m365-login-settings.php:47 msgid "Sign in with Microsoft" msgstr "" -#: includes/class-m365-login-settings.php:53 +#: includes/class-m365-login-settings.php:56 msgid "or" msgstr "" -#: includes/class-m365-login-settings.php:272 +#: includes/class-m365-login-settings.php:197 includes/class-m365-login-settings.php:445 +msgid "The private key could not be encrypted. Is the OpenSSL extension available?" +msgstr "" + +#: includes/class-m365-login-settings.php:391 msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"." msgstr "" -#: includes/class-m365-login-settings.php:280 +#: includes/class-m365-login-settings.php:399 msgid "The application (client) ID must be a GUID." msgstr "" -#: includes/class-m365-login-settings.php:292 +#: includes/class-m365-login-settings.php:411 msgid "The client secret contains invalid characters." msgstr "" -#: includes/class-m365-login-settings.php:296 +#: includes/class-m365-login-settings.php:415 msgid "The client secret could not be encrypted. Is the OpenSSL extension available?" msgstr "" -#: includes/class-m365-login-settings.php:346 +#: includes/class-m365-login-settings.php:435 +msgid "Please paste both the private key and the certificate." +msgstr "" + +#: includes/class-m365-login-settings.php:437 +msgid "The pasted key or certificate is too large." +msgstr "" + +#: includes/class-m365-login-settings.php:454 +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:500 msgid "The custom login page must be a URL on this site." msgstr "" diff --git a/m365-login.php b/m365-login.php index 8713c46..3729395 100644 --- a/m365-login.php +++ b/m365-login.php @@ -25,6 +25,7 @@ define( 'M365_LOGIN_OPTION', 'm365_login_settings' ); require_once M365_LOGIN_DIR . 'includes/class-m365-login-settings.php'; require_once M365_LOGIN_DIR . 'includes/class-m365-login-crypto.php'; require_once M365_LOGIN_DIR . 'includes/class-m365-login-jwt.php'; +require_once M365_LOGIN_DIR . 'includes/class-m365-login-certificate.php'; require_once M365_LOGIN_DIR . 'includes/class-m365-login-graph.php'; require_once M365_LOGIN_DIR . 'includes/class-m365-login-auth.php'; require_once M365_LOGIN_DIR . 'includes/class-m365-login-button.php'; diff --git a/readme.txt b/readme.txt index faa945b..b45fa64 100644 --- a/readme.txt +++ b/readme.txt @@ -33,7 +33,10 @@ The plugin is deliberately small and strict: * **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. -* The **client secret is encrypted at rest** (AES-256-GCM, key derived from your WordPress salts) and never displayed again. +* **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. +* Login starts and fallback-key attempts are rate limited per client. * Every setting is sanitised, every output escaped, every admin request nonce- and capability-checked. = Developer hooks = @@ -71,7 +74,7 @@ Microsoft terms and privacy: [Microsoft Services Agreement](https://www.microsof 2. Go to **Settings → M365 Login** and copy the **Redirect URI** shown in the sidebar. 3. In the [Microsoft Entra admin center](https://entra.microsoft.com/) open **App registrations → New registration**. Choose *Accounts in this organizational directory only*, select the **Web** platform and paste the redirect URI. 4. From the app's overview page copy the **Application (client) ID** and the **Directory (tenant) ID** into the plugin settings. -5. Under **Certificates & secrets** create a client secret and paste its *value* into the plugin settings. +5. Pick the authentication method: either generate a certificate in the plugin and upload the downloaded `.cer` under **Certificates & secrets → Certificates**, or create a client secret under **Certificates & secrets → Client secrets** and paste its *value*. Both methods have a step-by-step guide in the settings. 6. Under **Token configuration** add the optional claim **email** for ID tokens (recommended). The delegated permissions `openid`, `profile` and `email` are granted by default. 7. Save. The button now appears on `wp-login.php`. Customise it on the **Button** tab. @@ -79,6 +82,10 @@ Make sure every user who should be able to sign in has the same e-mail address i == Frequently Asked Questions == += Client secret or certificate? = + +Both work. A certificate is recommended: the private key stays on your server (encrypted), only the public certificate is uploaded to Entra ID, and every token request is signed with a short-lived assertion instead of sending a shared secret. The plugin generates the certificate for you and shows its expiry date. + = Does the plugin create users? = No. Users must already exist in WordPress. The e-mail address is the only link between the Microsoft account and the WordPress account. This is intentional – it keeps the administrator in control of who can access the site.