Add Entra group restriction, button-only mode and detailed README

Groups: a Graph-backed picker on the Security tab (search by name or
paste object IDs) stores allowed group IDs. During sign-in membership is
read from the ID token's groups claim when present, otherwise verified
through Microsoft Graph checkMemberGroups (transitive). Verification
failures refuse the sign-in.

Button-only mode: hides the password form and the lost-password link
and rejects password sign-ins on wp-login.php via the authenticate
filter. A generated, rate-limited fallback key re-enables the form for
30 minutes per browser; M365_LOGIN_DISABLE_BUTTON_ONLY switches the
mode off from wp-config.php.

Also: new German-language README with sequence diagram, settings
reference, troubleshooting and hook examples; readme.txt external
services section now covers Microsoft Graph; translations updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJxAHYdMfKPoN4koRc4Ci2
This commit is contained in:
friloo 2026-09-22 14:30:53 +00:00
parent 1517e7e3bc
commit 1202283eda
No known key found for this signature in database
20 changed files with 2241 additions and 517 deletions

64
.github/assets/button-preview.svg vendored Normal file
View file

@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="300" viewBox="0 0 720 300" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#f6f7f7"/>
<stop offset="1" stop-color="#e9edf2"/>
</linearGradient>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="6" stdDeviation="10" flood-color="#0b1d33" flood-opacity="0.12"/>
</filter>
</defs>
<rect width="720" height="300" rx="20" fill="url(#bg)"/>
<!-- Login card -->
<g transform="translate(200 34)" filter="url(#shadow)">
<rect width="320" height="232" rx="6" fill="#fff" stroke="#c3c4c7"/>
<!-- fake fields -->
<text x="24" y="38" font-size="13" fill="#3c434a">Benutzername oder E-Mail-Adresse</text>
<rect x="24" y="48" width="272" height="34" rx="4" fill="#fff" stroke="#8c8f94"/>
<text x="24" y="108" font-size="13" fill="#3c434a">Passwort</text>
<rect x="24" y="118" width="272" height="34" rx="4" fill="#fff" stroke="#8c8f94"/>
<!-- divider -->
<line x1="24" y1="180" x2="296" y2="180" stroke="#dcdcde"/>
<rect x="140" y="171" width="40" height="18" fill="#fff"/>
<text x="160" y="184" font-size="12" fill="#646970" text-anchor="middle">oder</text>
<!-- Microsoft button -->
<rect x="24" y="196" width="272" height="44" rx="4" fill="#2f2f2f"/>
<g transform="translate(70 206)">
<rect width="24" height="24" rx="2" fill="#fff"/>
<rect x="4" y="4" width="7" height="7" fill="#f25022"/>
<rect x="13" y="4" width="7" height="7" fill="#7fba00"/>
<rect x="4" y="13" width="7" height="7" fill="#00a4ef"/>
<rect x="13" y="13" width="7" height="7" fill="#ffb900"/>
</g>
<text x="180" y="223" font-size="15" font-weight="600" fill="#fff" text-anchor="middle">Login mit Microsoft</text>
</g>
<!-- Colour swatches -->
<g transform="translate(40 60)">
<text x="0" y="0" font-size="12" fill="#646970" font-weight="600">PRESETS</text>
<g transform="translate(0 16)">
<rect width="120" height="34" rx="17" fill="#2f2f2f"/><text x="60" y="22" font-size="12" fill="#fff" text-anchor="middle" font-weight="600">Microsoft dark</text>
</g>
<g transform="translate(0 60)">
<rect width="120" height="34" rx="17" fill="#fff" stroke="#8c8c8c"/><text x="60" y="22" font-size="12" fill="#5e5e5e" text-anchor="middle" font-weight="600">Microsoft light</text>
</g>
<g transform="translate(0 104)">
<rect width="120" height="34" rx="17" fill="#0078d4"/><text x="60" y="22" font-size="12" fill="#fff" text-anchor="middle" font-weight="600">Azure blue</text>
</g>
<g transform="translate(0 148)">
<rect width="120" height="34" rx="17" fill="#2271b1"/><text x="60" y="22" font-size="12" fill="#fff" text-anchor="middle" font-weight="600">WordPress blue</text>
</g>
</g>
<!-- Right: security badges -->
<g transform="translate(560 60)" font-size="12" fill="#1d2327">
<text x="0" y="0" font-size="12" fill="#646970" font-weight="600">SICHERHEIT</text>
<g transform="translate(0 16)"><circle cx="8" cy="8" r="8" fill="#1e6b31"/><path d="M4.5 8.5l2.5 2.5 4.5-5" fill="none" stroke="#fff" stroke-width="2"/><text x="24" y="12">PKCE (S256)</text></g>
<g transform="translate(0 44)"><circle cx="8" cy="8" r="8" fill="#1e6b31"/><path d="M4.5 8.5l2.5 2.5 4.5-5" fill="none" stroke="#fff" stroke-width="2"/><text x="24" y="12">State + Nonce</text></g>
<g transform="translate(0 72)"><circle cx="8" cy="8" r="8" fill="#1e6b31"/><path d="M4.5 8.5l2.5 2.5 4.5-5" fill="none" stroke="#fff" stroke-width="2"/><text x="24" y="12">JWKS-Signatur</text></g>
<g transform="translate(0 100)"><circle cx="8" cy="8" r="8" fill="#1e6b31"/><path d="M4.5 8.5l2.5 2.5 4.5-5" fill="none" stroke="#fff" stroke-width="2"/><text x="24" y="12">Tenant-Pinning</text></g>
<g transform="translate(0 128)"><circle cx="8" cy="8" r="8" fill="#1e6b31"/><path d="M4.5 8.5l2.5 2.5 4.5-5" fill="none" stroke="#fff" stroke-width="2"/><text x="24" y="12">Secret verschlüsselt</text></g>
<g transform="translate(0 156)"><circle cx="8" cy="8" r="8" fill="#1e6b31"/><path d="M4.5 8.5l2.5 2.5 4.5-5" fill="none" stroke="#fff" stroke-width="2"/><text x="24" y="12">Gruppen-Check</text></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -13,5 +13,7 @@ All notable changes to this project are documented in this file. The format foll
- ID token verification against Microsoft's JWKS (RS256, issuer, audience, tenant, expiry, nonce). - ID token verification against Microsoft's JWKS (RS256, issuer, audience, tenant, expiry, nonce).
- Encrypted client secret storage (AES-256-GCM). - Encrypted client secret storage (AES-256-GCM).
- Account binding to the Microsoft object ID, e-mail domain allow-list. - 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.
- `[m365_login_button]` shortcode and developer hooks. - `[m365_login_button]` shortcode and developer hooks.
- German translation. - German translation.

414
README.md
View file

@ -1,94 +1,394 @@
# M365 Login für WordPress <div align="center">
Ein schlankes, sicherheitsorientiertes WordPress-Plugin, das einen **„Login mit Microsoft“-Button** auf die <img src=".wordpress-org/icon.svg" width="96" height="96" alt="">
Anmeldeseite (`wp-login.php`) setzt. Bestehende WordPress-Benutzer melden sich mit ihrem Microsoft 365 /
Entra-ID-Konto an. Der gemeinsame Schlüssel ist die **E-Mail-Adresse** es werden keine Benutzer angelegt.
> Plugin-Slug / Text Domain: `m365-login` · Lizenz: GPL-2.0-or-later · PHP ≥ 7.4 · WordPress ≥ 6.0 # M365 Login
## Funktionen **Anmeldung an WordPress mit dem Microsoft 365 / Entra ID-Konto sicher, schlank, gestaltbar.**
- **Button auf der Login-Seite** Text, Icon (Microsoft-Logo oder eigenes Bild aus der Mediathek), Hintergrund-, [![WordPress](https://img.shields.io/badge/WordPress-6.0%2B-21759b?logo=wordpress&logoColor=white)](https://wordpress.org/)
Hover-, Text- und Rahmenfarbe, Eckenradius und Position (über/unter dem Formular) sind im Backend einstellbar, [![PHP](https://img.shields.io/badge/PHP-7.4%2B-777bb4?logo=php&logoColor=white)](https://www.php.net/)
mit Live-Vorschau und Farb-Presets. [![License](https://img.shields.io/badge/Lizenz-GPL--2.0--or--later-blue.svg)](LICENSE)
- **Aufgeräumte Einstellungsseite** unter *Einstellungen → M365 Login* mit Redirect-URI zum Kopieren, [![CI](https://github.com/friloo/wp-m365-login/actions/workflows/ci.yml/badge.svg)](https://github.com/friloo/wp-m365-login/actions/workflows/ci.yml)
Tenant-Verbindungstest und 5-Schritte-Anleitung. [![Plugin Check](https://img.shields.io/badge/WordPress.org-Plugin%20Check%20ready-46b450)](docs/wordpress-org-einreichung.md)
- **Kein Provisioning**: Anmeldung nur, wenn ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert.
- **Shortcode** `[m365_login_button redirect="/mein-konto/"]` für eigene Login-Seiten.
- Vollständig übersetzbar, deutsche Übersetzung enthalten.
## Sicherheit <img src=".github/assets/button-preview.svg" width="720" alt="Login-Seite mit Microsoft-Button, Farb-Presets und Sicherheitsmerkmalen">
| Maßnahme | Umsetzung | </div>
---
## Inhalt
- [Auf einen Blick](#auf-einen-blick)
- [So funktioniert es](#so-funktioniert-es)
- [Installation](#installation)
- [Einrichtung in Microsoft Entra ID](#einrichtung-in-microsoft-entra-id)
- [Einstellungen im Backend](#einstellungen-im-backend)
- [Verbindung](#verbindung)
- [Button](#button)
- [Sicherheit](#sicherheit)
- [Gruppen-Beschränkung](#gruppen-beschränkung)
- [Nur-Button-Modus & Fallback](#nur-button-modus--fallback)
- [Sicherheitskonzept](#sicherheitskonzept)
- [Shortcode & Hooks](#shortcode--hooks)
- [Fehlerbehebung](#fehlerbehebung)
- [Entwicklung](#entwicklung)
- [Einreichung bei WordPress.org](#einreichung-bei-wordpressorg)
- [FAQ](#faq)
- [Lizenz](#lizenz)
---
## Auf einen Blick
| | |
| --- | --- | | --- | --- |
| Authorization Code Flow **mit PKCE (S256)** | Tokens laufen ausschließlich Server-zu-Server, nie durch den Browser. | | 🔑 **Login per Microsoft** | Ein Klick auf der Anmeldeseite, Anmeldung bei Microsoft, zurück in WordPress fertig. |
| **State & Nonce** | Einmalig, 10 Minuten gültig, per HttpOnly/SameSite-Cookie an den Browser gebunden (CSRF-/Replay-Schutz, verhindert Login-CSRF). | | 📧 **Zuordnung über die E-Mail-Adresse** | Es werden **keine Benutzer angelegt**. Nur wer schon ein WordPress-Konto mit derselben E-Mail hat, kommt rein. |
| **ID-Token-Prüfung** | Signatur gegen Microsofts JWKS (RS256, Schlüssel-Rollover wird abgefangen), Issuer, Audience, Tenant, `exp`/`nbf`/`iat`, Nonce. `alg=none`/HMAC werden abgelehnt. | | 🎨 **Gestaltbarer Button** | Text, Icon (Microsoft-Logo oder eigenes Bild), Farben, Hover-Farbe, Rahmen, Eckenradius, Position mit Live-Vorschau und Presets. |
| **Tenant-Pinning** | Bei konfigurierter Tenant-GUID werden Tokens anderer Tenants abgewiesen. | | 👥 **Entra-Gruppen** | Optional nur Mitglieder ausgewählter Gruppen zulassen. Gruppen werden direkt im Backend gesucht und ausgewählt. |
| **Konto-Bindung** | Beim ersten Login wird die unveränderliche Microsoft-Objekt-ID am Benutzer gespeichert; spätere Logins mit gleicher E-Mail, aber anderer Identität werden abgelehnt. | | 🚪 **Nur-Button-Modus** | Passwortfelder ausblenden und Passwort-Logins sperren mit geheimem Fallback-Link als Notausgang. |
| **Domain-Allowlist** | Optional nur bestimmte E-Mail-Domains zulassen. | | 🛡️ **Sicher by default** | OpenID Connect + PKCE, Signaturprüfung, Tenant-Pinning, Konto-Bindung, verschlüsseltes Secret. |
| **Client Secret verschlüsselt** | AES-256-GCM, Schlüssel aus den WordPress-Salts abgeleitet; wird nie wieder angezeigt. | | 🌍 **Übersetzbar** | Englische Basis, deutsche Übersetzung (du & Sie) enthalten. |
| **WordPress-Standards** | Capability-Checks, Nonces, Sanitizing aller Eingaben, Escaping aller Ausgaben, `wp_safe_redirect`, keine externen Assets. | | 📦 **WordPress.org-ready** | readme.txt, Lizenz, Uninstall, Plugin Check in CI, Build-Script. |
## Installation & Einrichtung ---
1. Ordner in `wp-content/plugins/` legen (oder ZIP aus `bin/build-zip.sh` hochladen) und aktivieren. ## So funktioniert es
2. *Einstellungen → M365 Login* öffnen und die **Redirect-URI** aus der Seitenleiste kopieren
(`https://deine-seite.tld/m365-login/callback`).
3. Im [Microsoft Entra Admin Center](https://entra.microsoft.com/) → **App-Registrierungen → Neue Registrierung**:
- Name frei wählbar, z. B. „WordPress Login“.
- Kontotypen: *Nur Konten in diesem Organisationsverzeichnis* (Single Tenant).
- Plattform **Web**, Redirect-URI einfügen.
4. Auf der Übersichtsseite **Anwendungs-ID (Client)** und **Verzeichnis-ID (Mandant)** kopieren und im Plugin eintragen.
5. **Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel** den *Wert* (nicht die ID) ins Plugin eintragen.
Ablaufdatum notieren; abgelaufene Secrets müssen erneuert werden.
6. **Tokenkonfiguration → Optionalen Anspruch hinzufügen → ID → `email`** (empfohlen). Die delegierten
Berechtigungen `openid`, `profile`, `email` sind standardmäßig vorhanden.
7. Speichern. Der Button erscheint auf `wp-login.php`; Gestaltung im Tab **Button**.
Stelle sicher, dass die E-Mail-Adressen der WordPress-Benutzer mit denen in Microsoft 365 übereinstimmen. ```mermaid
sequenceDiagram
autonumber
participant B as Browser
participant WP as WordPress<br/>(M365 Login)
participant MS as Microsoft Entra ID
participant G as Microsoft Graph<br/>(optional)
## Entwickler-Hooks B->>WP: Klick auf „Login mit Microsoft“
WP->>WP: state, nonce, PKCE-Verifier erzeugen<br/>State-Cookie setzen (HttpOnly)
WP-->>B: Redirect zu Microsoft (code_challenge, state, nonce)
B->>MS: Anmeldung beim Microsoft-Konto
MS-->>B: Redirect zurück mit code + state
B->>WP: /m365-login/callback?code=…&state=…
WP->>WP: State einmalig einlösen, Cookie prüfen
WP->>MS: Code + code_verifier + Client Secret (Server-zu-Server)
MS-->>WP: ID-Token
WP->>MS: Signaturschlüssel (JWKS, gecacht)
WP->>WP: Signatur, Issuer, Audience, Tenant, exp, Nonce prüfen
opt Gruppen-Beschränkung aktiv
WP->>G: checkMemberGroups(oid, erlaubte Gruppen)
G-->>WP: Treffer / kein Treffer
end
WP->>WP: Benutzer per E-Mail suchen, Objekt-ID abgleichen
WP-->>B: WordPress-Session, Redirect ins Dashboard
```
Tokens laufen ausschließlich zwischen deinem Server und Microsoft. Der Browser sieht nur einen Autorisierungscode, der ohne den serverseitigen PKCE-Verifier und das Client Secret wertlos ist.
---
## Installation
**Variante A manuell (empfohlen, solange das Plugin nicht im Verzeichnis ist)**
```bash
git clone https://github.com/friloo/wp-m365-login.git
cd wp-m365-login
bash bin/build-zip.sh # erzeugt build/m365-login.zip
```
Dann in WordPress unter *Plugins → Installieren → Plugin hochladen* das ZIP hochladen und aktivieren.
Alternativ den Repo-Inhalt als Ordner `m365-login` nach `wp-content/plugins/` kopieren.
**Variante B WordPress.org** (nach der Freigabe): *Plugins → Installieren → „M365 Login“*.
> **Voraussetzungen:** WordPress ≥ 6.0, PHP ≥ 7.4 mit OpenSSL-Erweiterung, HTTPS auf der Website (Microsoft akzeptiert `http://` nur für `localhost`).
---
## Einrichtung in Microsoft Entra ID
<details open>
<summary><strong>Schritt für Schritt (ca. 5 Minuten)</strong></summary>
1. **Redirect-URI kopieren.** In WordPress *Einstellungen → M365 Login* öffnen; die URI steht in der Seitenleiste
(`https://deine-seite.tld/m365-login/callback`, bei einfachen Permalinks `https://deine-seite.tld/?m365-login=callback`).
2. **App registrieren.** [Microsoft Entra Admin Center](https://entra.microsoft.com/) → *App-Registrierungen → Neue Registrierung*
- Name: z. B. „WordPress Login“
- Unterstützte Kontotypen: **Nur Konten in diesem Organisationsverzeichnis** (Single Tenant)
- Umleitungs-URI: Plattform **Web**, URI aus Schritt 1
3. **IDs übernehmen.** Auf der Übersichtsseite **Anwendungs-ID (Client)** und **Verzeichnis-ID (Mandant)** kopieren → in WordPress eintragen.
4. **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.
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.
</details>
<details>
<summary><strong>Zusätzlich für die Gruppen-Beschränkung</strong></summary>
Damit das Backend Gruppen suchen und beim Login die Mitgliedschaft prüfen kann, braucht die App-Registrierung
**Anwendungsberechtigungen** (nicht delegiert) für Microsoft Graph, jeweils mit **Administratorzustimmung**:
| Berechtigung | Wofür |
| --- | --- |
| `GroupMember.Read.All` | Gruppen im Backend suchen |
| `User.Read.All` | Mitgliedschaft beim Login prüfen (`checkMemberGroups`, inkl. verschachtelter Gruppen) |
`Directory.Read.All` deckt beides ab, ist aber weiter gefasst.
**Ohne Graph-Berechtigungen** geht es auch: Unter *Tokenkonfiguration → Gruppenanspruch hinzufügen* den `groups`-Claim
für ID-Tokens aktivieren (am besten *Der Anwendung zugewiesene Gruppen* oder *Sicherheitsgruppen*). Dann prüft das Plugin die
Mitgliedschaft direkt im Token. Gruppen-IDs lassen sich im Backend auch von Hand einfügen. Bei mehr als 200 Gruppen pro
Benutzer liefert Microsoft keinen `groups`-Claim mehr („Overage“); dann greift das Plugin automatisch auf Graph zurück.
</details>
Wichtig: Jeder Benutzer, der sich per Microsoft anmelden soll, braucht in WordPress **dieselbe E-Mail-Adresse** wie in Microsoft 365.
---
## Einstellungen im Backend
*Einstellungen → M365 Login* drei Tabs, ein Formular, ein Speichern-Button.
### Verbindung
| Feld | Beschreibung |
| --- | --- |
| Verzeichnis-ID (Tenant) | GUID des Tenants (empfohlen, aktiviert Tenant-Pinning) oder `organizations` / `common` / `consumers`. |
| Anwendungs-ID (Client) | GUID der App-Registrierung. |
| Client Secret | Wird verschlüsselt gespeichert und nie wieder angezeigt. Leer lassen = behalten. |
| Kontoauswahl | `select_account` (Standard), `none` (bestehende Microsoft-Sitzung nutzen) oder `login` (immer Anmeldedaten verlangen). |
| Tenant testen | Lädt die OpenID-Konfiguration des Tenants prüft ID und ausgehende Verbindung. |
### Button
| Option | Beschreibung |
| --- | --- |
| Button-Text | Standard „Sign in with Microsoft“ / „Login mit Microsoft“ (max. 80 Zeichen). |
| Trennlinien-Text | Standard „or“ / „oder“; leer = keine Trennlinie. |
| Icon | Microsoft-Logo (eingebettet) oder eigenes Bild aus der Mediathek (PNG, SVG, JPG, WebP). Ein-/ausblendbar. |
| Farben | Hintergrund, Hintergrund (Hover), Text, Rahmen mit Farbwähler. |
| Eckenradius | 050 px. |
| Position | Unter dem Login-Formular (Standard) oder darüber. |
| Presets | Microsoft dunkel, Microsoft hell, Azure-Blau, WordPress-Blau. |
Alles wird live in der Vorschau angezeigt, bevor du speicherst.
### Sicherheit
| Option | Standard | Beschreibung |
| --- | --- | --- |
| Konto an Microsoft-Objekt-ID binden | an | Beim ersten Login wird die `oid` gespeichert; danach muss sie übereinstimmen. Schützt vor Übernahme, wenn eine E-Mail-Adresse in Microsoft neu vergeben wird. |
| UPN-Fallback | an | Fehlt der `email`-Claim, wird der User Principal Name verwendet, sofern er eine gültige E-Mail-Adresse ist. |
| Angemeldet bleiben | aus | 14-Tage-Session statt Browser-Session. |
| Erlaubte E-Mail-Domains | leer | Kommagetrennte Liste, z. B. `contoso.com, contoso.de`. |
### Gruppen-Beschränkung
Im Tab *Sicherheit***Erlaubte Entra-Gruppen**:
1. Gruppenname eintippen (oder Objekt-ID einfügen) → *Suchen*.
2. Treffer mit *Hinzufügen* übernehmen sie erscheinen als Chips mit Name und ID.
3. Speichern. Ab jetzt darf sich nur anmelden, wer in **mindestens einer** dieser Gruppen ist (verschachtelte Mitgliedschaften zählen).
Prüfreihenfolge beim Login:
1. Enthält das ID-Token einen `groups`-Claim → Abgleich direkt im Token.
2. Sonst (oder bei Overage) → Microsoft Graph `checkMemberGroups`.
3. Schlägt beides fehl → **Anmeldung abgelehnt** (fail closed), Meldung „Gruppenmitgliedschaft konnte nicht geprüft werden“.
Leere Liste = keine Beschränkung.
### Nur-Button-Modus & Fallback
Im Tab *Sicherheit***Button-only mode**:
- Blendet Benutzername/Passwort-Felder und den „Passwort vergessen?“-Link aus.
- **Sperrt Passwort-Logins** über `wp-login.php` serverseitig nicht nur per CSS.
- Application Passwords, REST API und XML-RPC sind nicht betroffen.
- Wird erst aktiv, wenn die Verbindung vollständig konfiguriert ist.
**Fallback (Notausgang):** Beim Speichern erzeugt das Plugin einen geheimen Schlüssel und zeigt den Fallback-Link an:
```
https://deine-seite.tld/wp-login.php?m365_fallback=AbC…xYz
```
Wer den Link öffnet, sieht für **30 Minuten in diesem Browser** wieder das normale Formular und kann sich mit Passwort anmelden.
Der Schlüssel landet nicht im Cookie (nur ein HMAC davon), Fehlversuche werden pro IP gedrosselt (10 Versuche / 15 Minuten),
und über die Checkbox *Neuen Schlüssel beim Speichern erzeugen* lässt er sich jederzeit rotieren.
**Notschalter ohne Backend-Zugang:** In `wp-config.php`
```php ```php
// Button z. B. nur für eine bestimmte Domain anzeigen define( 'M365_LOGIN_DISABLE_BUTTON_ONLY', true );
add_filter( 'm365_login_show_button', fn( $show ) => $show && 'intranet.example.com' === $_SERVER['HTTP_HOST'] ); ```
// domain_hint an Microsoft senden schaltet den Modus komplett ab. Alternativ das Plugin-Verzeichnis per FTP umbenennen.
> ⚠️ Vor dem Aktivieren sicherstellen, dass dein eigenes Admin-Konto per Microsoft funktioniert, und den Fallback-Link sicher ablegen.
---
## Sicherheitskonzept
| Bedrohung | Gegenmaßnahme |
| --- | --- |
| Abfangen von Tokens im Browser | Authorization Code Flow **mit PKCE (S256)**; ID-Token wird serverseitig geholt, `response_mode=query` ohne Token. |
| CSRF / Login-CSRF | `state` ist zufällig (256 Bit), **einmalig** verwendbar, 10 Min. gültig und per HttpOnly-/SameSite-Cookie an den startenden Browser gebunden. |
| Token-Replay | `nonce` wird im ID-Token geprüft und mit dem State-Datensatz verworfen. |
| Gefälschte Tokens | Signaturprüfung gegen Microsofts JWKS (`RS256` only; `alg=none`/HMAC werden abgelehnt), Schlüssel-Rollover wird automatisch nachgeladen. `iss`, `aud`, `tid`, `exp`, `nbf`, `iat` werden geprüft. |
| Fremde Tenants | Bei konfigurierter Tenant-GUID **Tenant-Pinning**; sonst Issuer-Konsistenz mit `tid`. |
| Kontoübernahme per E-Mail-Recycling | **Bindung an die Objekt-ID** (`oid`) beim ersten Login. |
| Unbefugte Konten | 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. |
| 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).
---
## Shortcode & Hooks
**Shortcode** für eigene Login-Seiten:
```
[m365_login_button redirect="/mein-konto/"]
```
**Filter & Actions**
```php
// Button z. B. nur im Intranet zeigen
add_filter( 'm365_login_show_button', function ( $show ) {
return $show && 'intranet.example.com' === $_SERVER['HTTP_HOST'];
} );
// domain_hint mitschicken, damit Microsoft direkt die Firmenanmeldung zeigt
add_filter( 'm365_login_authorize_params', function ( $params ) { add_filter( 'm365_login_authorize_params', function ( $params ) {
$params['domain_hint'] = 'contoso.com'; $params['domain_hint'] = 'contoso.com';
return $params; return $params;
} ); } );
// Login zusätzlich anhand der Claims verbieten (z. B. Gruppenmitgliedschaft) // E-Mail vor dem Lookup umschreiben (z. B. Alias-Domain)
add_filter( 'm365_login_match_email', function ( $email, $claims ) {
return str_replace( '@alt.contoso.com', '@contoso.com', $email );
}, 10, 2 );
// Eigene Zusatzprüfung nach allen Plugin-Checks
add_filter( 'm365_login_allow_user', function ( $allowed, WP_User $user, array $claims ) { add_filter( 'm365_login_allow_user', function ( $allowed, WP_User $user, array $claims ) {
return $allowed && ! empty( $claims['groups'] ); return $allowed && ! in_array( 'subscriber', $user->roles, true );
}, 10, 3 ); }, 10, 3 );
// Nach erfolgreichem Login, z. B. Anzeigenamen synchronisieren
add_action( 'm365_login_success', function ( WP_User $user, array $claims ) { add_action( 'm365_login_success', function ( WP_User $user, array $claims ) {
// z. B. Anzeigenamen synchronisieren if ( ! empty( $claims['name'] ) ) {
wp_update_user( array( 'ID' => $user->ID, 'display_name' => $claims['name'] ) );
}
}, 10, 2 ); }, 10, 2 );
// Redirect-URI anpassen (z. B. hinter einem Reverse Proxy)
add_filter( 'm365_login_redirect_uri', fn( $uri ) => 'https://www.example.com/m365-login/callback' );
``` ```
Weitere: `m365_login_match_email` (E-Mail vor dem Lookup anpassen). ---
## Fehlerbehebung
| Meldung auf der Login-Seite | Ursache & Lösung |
| --- | --- |
| *Microsoft login is not configured yet.* | Tenant-ID, Client-ID oder Secret fehlt. |
| *The login request expired or was invalid.* | State abgelaufen (> 10 Min.), Cookie blockiert oder Seite doppelt geladen. Erneut versuchen; Cookies für die Domain erlauben. |
| *Could not complete the sign-in with Microsoft.* | Token-Tausch fehlgeschlagen meist falsches/abgelaufenes Client Secret oder Redirect-URI stimmt nicht exakt mit Entra überein. Details im Log. |
| *The Microsoft sign-in could not be verified.* | ID-Token abgelehnt (Tenant, Audience, Signatur). Tenant-ID prüfen; Serverzeit prüfen (NTP). |
| *Your Microsoft account did not provide an e-mail address.* | `email`-Claim fehlt und UPN-Fallback ist aus oder UPN ist keine E-Mail. Claim in der Tokenkonfiguration hinzufügen. |
| *No WordPress account exists for your Microsoft e-mail address.* | E-Mail in WordPress stimmt nicht mit Microsoft überein. |
| *This WordPress account is linked to a different Microsoft account.* | Objekt-ID weicht ab. Wenn gewollt (neues Microsoft-Konto): User-Meta `_m365_login_oid` beim Benutzer löschen. |
| *… not a member of a group that is allowed …* | Benutzer ist in keiner der ausgewählten Gruppen. |
| *Your group membership could not be verified.* | Graph nicht erreichbar oder Berechtigung fehlt (`User.Read.All`) oder `groups`-Claim aktivieren. |
Logging: Mit `WP_DEBUG` und `WP_DEBUG_LOG` schreibt das Plugin Fehlerdetails mit Präfix `[M365 Login]` nach `wp-content/debug.log`. Es werden nie Tokens oder Secrets geloggt.
---
## Entwicklung ## Entwicklung
```bash ```
composer install # PHPCS + WordPress Coding Standards m365-login.php Plugin-Header & Bootstrap
composer lint # php -l über alle Dateien includes/
composer phpcs # Coding-Standards-Prüfung class-m365-login.php Verdrahtung der Komponenten
bash bin/build-zip.sh # build/m365-login.zip für Upload/Einreichung class-m365-login-settings.php Defaults, Sanitizing, Redirect-URI, Fallback-Key
python3 bin/compile-mo.py # languages/*.po → *.mo class-m365-login-crypto.php AES-256-GCM für das Client Secret
class-m365-login-jwt.php RS256-Verifikation, JWKS → PEM
class-m365-login-auth.php OAuth-Flow, Callback, Benutzerzuordnung, Nur-Button-Modus
class-m365-login-graph.php Client-Credentials-Token, Gruppensuche, checkMemberGroups
class-m365-login-button.php Ausgabe auf wp-login.php, Shortcode
class-m365-login-admin.php Einstellungsseite, AJAX
assets/ CSS/JS für Login-Seite und Backend (unminifiziert)
languages/ .pot, de_DE, de_DE_formal
bin/ build-zip.sh, make-pot.py, compile-mo.py
docs/ Einreichungs-Checkliste
``` ```
Die GitHub-Actions-Pipeline (`.github/workflows/ci.yml`) führt Syntax-Check (PHP 7.48.4), PHPCS und den ```bash
offiziellen **WordPress Plugin Check** aus. composer install # PHPCS + WordPress Coding Standards + PHPCompatibility
composer lint # php -l für alle Dateien
composer phpcs # Coding-Standards-Prüfung (phpcs.xml.dist)
python3 bin/make-pot.py # Strings extrahieren (oder: wp i18n make-pot . languages/m365-login.pot)
python3 bin/compile-mo.py # .po → .mo
bash bin/build-zip.sh # build/m365-login.zip
```
Die CI (`.github/workflows/ci.yml`) prüft Syntax unter PHP 7.48.4, führt PHPCS aus und lässt den offiziellen
**WordPress Plugin Check** über das Build-Verzeichnis laufen.
---
## Einreichung bei WordPress.org ## Einreichung bei WordPress.org
Siehe [docs/wordpress-org-einreichung.md](docs/wordpress-org-einreichung.md) für die vollständige Checkliste. Das Plugin bringt alles mit, was das Review-Team verlangt: `readme.txt` mit *External services*-Abschnitt, GPL-Lizenz,
`uninstall.php`, eindeutige Präfixe, keine externen Assets, Übersetzungen, Verzeichnis-Icon. Die komplette Checkliste
(inkl. Slug-/Marken-Hinweisen und SVN-Schritten nach der Freigabe) steht in
**[docs/wordpress-org-einreichung.md](docs/wordpress-org-einreichung.md)**.
---
## FAQ
<details>
<summary><strong>Kann ich Benutzer automatisch anlegen lassen?</strong></summary>
Nein, bewusst nicht. Der Admin entscheidet, wer ein Konto hat. Wer Auto-Provisioning braucht, kann es über den Hook <code>m365_login_allow_user</code> nicht nachrüsten das wäre ein anderes Sicherheitsmodell.
</details>
<details>
<summary><strong>Funktioniert es mit privaten Microsoft-Konten (outlook.com)?</strong></summary>
Ja, Tenant auf <code>consumers</code> oder <code>common</code> stellen. Microsoft erlaubt dann keine Query-Strings in Redirect-URIs, deshalb müssen sprechende Permalinks aktiv sein (Callback ohne <code>?</code>).
</details>
<details>
<summary><strong>Multisite?</strong></summary>
Ja. Einstellungen gelten pro Site; der Benutzer muss Mitglied der Site (oder Super-Admin) sein.
</details>
<details>
<summary><strong>Was passiert beim Deinstallieren?</strong></summary>
Einstellungen, Caches (Transients) und die pro Benutzer gespeicherte Objekt-ID werden entfernt auch in Multisite.
</details>
<details>
<summary><strong>Ich habe mich ausgesperrt.</strong></summary>
Fallback-Link öffnen. Kein Link zur Hand? <code>define( 'M365_LOGIN_DISABLE_BUTTON_ONLY', true );</code> in die <code>wp-config.php</code> oder den Plugin-Ordner per FTP umbenennen.
</details>
---
## Lizenz ## Lizenz
GPL-2.0-or-later siehe [LICENSE](LICENSE). GPL-2.0-or-later siehe [LICENSE](LICENSE). „Microsoft“, „Microsoft 365“ und das Microsoft-Logo sind Marken der Microsoft Corporation;
das Plugin ist ein unabhängiges Community-Projekt und steht in keiner Verbindung zu Microsoft.

View file

@ -576,3 +576,145 @@
.m365-card code { .m365-card code {
font-size: 12px; font-size: 12px;
} }
/* Group picker */
.m365-group-results {
margin-top: 10px;
border: 1px solid var(--m365-border);
border-radius: 8px;
max-height: 320px;
overflow-y: auto;
background: #fff;
}
.m365-group-results.is-error {
border-color: #f0b8bd;
background: #fcf0f1;
}
.m365-group-results__status {
margin: 0;
padding: 12px 14px;
color: var(--m365-muted);
}
.m365-group-result {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
border-bottom: 1px solid #f0f0f1;
}
.m365-group-result:last-child {
border-bottom: 0;
}
.m365-group-result__meta {
min-width: 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px 10px;
}
.m365-group-result__meta code {
font-size: 11px;
color: var(--m365-muted);
background: transparent;
padding: 0;
}
.m365-group-result__meta em {
display: block;
width: 100%;
font-style: normal;
color: var(--m365-muted);
font-size: 12px;
}
.m365-group-result__type {
font-size: 11px;
padding: 1px 8px;
border-radius: 999px;
background: #f0f6fc;
color: #0a4b78;
}
.m365-group-list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.m365-group-list.is-empty::before {
content: attr(data-empty);
color: var(--m365-muted);
font-size: 13px;
}
.m365-group-chip {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0;
padding: 6px 6px 6px 12px;
background: #f0f6fc;
border: 1px solid #c5d9ed;
border-radius: 999px;
font-size: 13px;
max-width: 100%;
}
.m365-group-chip__name {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.m365-group-chip__id {
font-size: 11px;
color: var(--m365-muted);
background: transparent;
padding: 0;
}
.m365-group-chip__remove {
appearance: none;
border: 0;
background: #fff;
color: #b32d2e;
width: 22px;
height: 22px;
border-radius: 50%;
line-height: 1;
font-size: 16px;
cursor: pointer;
}
.m365-group-chip__remove:hover {
background: #b32d2e;
color: #fff;
}
/* Fallback link */
.m365-fallback {
margin-top: 8px;
padding: 16px;
border: 1px dashed #c3c4c7;
border-radius: 8px;
background: #fafafa;
}
.m365-fallback .description {
margin: 4px 0 8px;
}
.m365-fallback .m365-copy {
margin-bottom: 8px;
}

View file

@ -120,3 +120,17 @@ body.m365-login-attached #loginform {
background: transparent; background: transparent;
max-width: 360px; max-width: 360px;
} }
/* Button-only mode: the password form is hidden until the fallback key is used. */
body.m365-button-only #loginform,
body.m365-button-only #nav {
display: none;
}
body.m365-button-only .m365-login--below,
body.m365-button-only .m365-login--above {
margin-top: 20px;
padding: 26px 24px;
border: 1px solid #c3c4c7;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}

View file

@ -159,6 +159,110 @@
} }
} ); } );
/* ---------------- Entra group picker ---------------- */
var $groupList = $( '#m365-group-list' );
var $groupResults = $( '#m365-group-results' );
var optionName = ( $( '#m365-tenant' ).attr( 'name' ) || '' ).replace( /\[tenant_id\]$/, '' );
function escapeHtml( str ) {
return $( '<div>' ).text( str || '' ).html();
}
function refreshGroupList() {
$groupList.toggleClass( 'is-empty', 0 === $groupList.children( 'li' ).length );
}
function addGroup( id, name ) {
id = ( id || '' ).toLowerCase();
if ( ! /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test( id ) ) {
return;
}
if ( $groupList.find( 'li[data-id="' + id + '"]' ).length ) {
return;
}
var $li = $( '<li class="m365-group-chip">' ).attr( 'data-id', id );
$li.append( $( '<span class="m365-group-chip__name">' ).text( name || id ) );
$li.append( $( '<code class="m365-group-chip__id">' ).text( id ) );
$li.append( $( '<input type="hidden">' ).attr( 'name', optionName + '[allowed_groups][' + id + ']' ).val( name || id ) );
$li.append( $( '<button type="button" class="m365-group-chip__remove" aria-label="' + escapeHtml( i18n.remove ) + '">&times;</button>' ) );
$groupList.append( $li );
refreshGroupList();
}
$groupList.on( 'click', '.m365-group-chip__remove', function () {
$( this ).closest( 'li' ).remove();
refreshGroupList();
} );
refreshGroupList();
function searchGroups() {
var query = $.trim( $( '#m365-group-search' ).val() );
$groupResults.prop( 'hidden', false ).removeClass( 'is-error' ).html( '<p class="m365-group-results__status">' + escapeHtml( i18n.searching ) + '</p>' );
$.post( cfg.ajaxUrl, {
action: cfg.groupAction,
nonce: cfg.nonce,
query: query
} ).done( function ( res ) {
if ( ! res || ! res.success ) {
var msg = ( res && res.data && res.data.message ) || i18n.testFailed;
$groupResults.addClass( 'is-error' ).html( '<p class="m365-group-results__status">' + escapeHtml( msg ) + '</p>' );
// Allow adding a pasted GUID even when Graph is unavailable.
if ( /^[0-9a-f-]{36}$/i.test( query ) ) {
$groupResults.append( buildResult( { id: query, name: query, type: '', description: '' } ) );
}
return;
}
var groups = res.data.groups || [];
if ( ! groups.length ) {
$groupResults.html( '<p class="m365-group-results__status">' + escapeHtml( i18n.noGroups ) + '</p>' );
return;
}
$groupResults.empty();
$.each( groups, function ( i, g ) {
$groupResults.append( buildResult( g ) );
} );
} ).fail( function () {
$groupResults.addClass( 'is-error' ).html( '<p class="m365-group-results__status">' + escapeHtml( i18n.testFailed ) + '</p>' );
} );
}
function buildResult( g ) {
var $row = $( '<div class="m365-group-result">' );
var $meta = $( '<div class="m365-group-result__meta">' );
$meta.append( $( '<strong>' ).text( g.name ) );
if ( g.type ) {
$meta.append( $( '<span class="m365-group-result__type">' ).text( g.type ) );
}
$meta.append( $( '<code>' ).text( g.id ) );
if ( g.description ) {
$meta.append( $( '<em>' ).text( g.description ) );
}
var $btn = $( '<button type="button" class="button button-small">' ).text( i18n.add ).on( 'click', function () {
addGroup( g.id, g.name );
$( this ).prop( 'disabled', true );
} );
if ( $groupList.find( 'li[data-id="' + ( g.id || '' ).toLowerCase() + '"]' ).length ) {
$btn.prop( 'disabled', true );
}
return $row.append( $meta ).append( $btn );
}
$( '#m365-group-search-btn' ).on( 'click', searchGroups );
$( '#m365-group-search' ).on( 'keydown', function ( e ) {
if ( 'Enter' === e.key ) {
e.preventDefault();
searchGroups();
}
} );
/* ---------------- Fallback key ---------------- */
$( '#m365-fallback-regenerate' ).on( 'change', function () {
if ( this.checked && ! window.confirm( i18n.confirmKey ) ) {
this.checked = false;
}
} );
/* ---------------- Test tenant ---------------- */ /* ---------------- Test tenant ---------------- */
$( '#m365-test' ).on( 'click', function () { $( '#m365-test' ).on( 'click', function () {
var $btn = $( this ); var $btn = $( this );

View file

@ -24,8 +24,8 @@ erfüllt. Vor der Einreichung unter <https://wordpress.org/plugins/developers/ad
| Anforderung | Umsetzung | | Anforderung | Umsetzung |
| --- | --- | | --- | --- |
| GPL-kompatible Lizenz | `LICENSE` (GPL-2.0), Header in `m365-login.php`, `readme.txt` | | GPL-kompatible Lizenz | `LICENSE` (GPL-2.0), Header in `m365-login.php`, `readme.txt` |
| `readme.txt` im WP-Format | inkl. Pflichtabschnitt **External services** (Microsoft-Endpunkte, übertragene Daten, Links zu Nutzungsbedingungen/Datenschutz) | | `readme.txt` im WP-Format | inkl. Pflichtabschnitt **External services** (Microsoft-Login- und Graph-Endpunkte, übertragene Daten, Links zu Nutzungsbedingungen/Datenschutz) |
| Keine externen Assets/CDNs | CSS/JS liegen im Plugin; einzige Netzwerkverbindungen gehen zu `login.microsoftonline.com` | | Keine externen Assets/CDNs | CSS/JS liegen im Plugin; einzige Netzwerkverbindungen gehen zu `login.microsoftonline.com` und (optional, Gruppenprüfung) `graph.microsoft.com` |
| Sanitizing / Escaping / Nonces | Settings API mit `sanitize_callback`, `esc_*` bei jeder Ausgabe, `check_ajax_referer` + `current_user_can` | | Sanitizing / Escaping / Nonces | Settings API mit `sanitize_callback`, `esc_*` bei jeder Ausgabe, `check_ajax_referer` + `current_user_can` |
| Eindeutiges Präfix | `m365_login_` / `M365_Login_` für alle globalen Bezeichner | | Eindeutiges Präfix | `m365_login_` / `M365_Login_` für alle globalen Bezeichner |
| Kein Tracking, keine Telefon-nach-Hause-Funktion | | | Kein Tracking, keine Telefon-nach-Hause-Funktion | |

View file

@ -14,8 +14,9 @@ class M365_Login_Admin {
const PAGE = 'm365-login'; const PAGE = 'm365-login';
const GROUP = 'm365_login'; const GROUP = 'm365_login';
const AJAX_TEST = 'm365_login_test_connection'; const AJAX_TEST = 'm365_login_test_connection';
const NONCE_TEST = 'm365_login_test'; const AJAX_GROUPS = 'm365_login_search_groups';
const NONCE_TEST = 'm365_login_test';
/** /**
* Settings. * Settings.
@ -31,6 +32,13 @@ class M365_Login_Admin {
*/ */
private $auth; private $auth;
/**
* Graph client.
*
* @var M365_Login_Graph
*/
private $graph;
/** /**
* Screen hook suffix. * Screen hook suffix.
* *
@ -43,15 +51,19 @@ class M365_Login_Admin {
* *
* @param M365_Login_Settings $settings Settings. * @param M365_Login_Settings $settings Settings.
* @param M365_Login_Auth $auth Auth. * @param M365_Login_Auth $auth Auth.
* @param M365_Login_Graph $graph Graph client.
*/ */
public function __construct( M365_Login_Settings $settings, M365_Login_Auth $auth ) { public function __construct( M365_Login_Settings $settings, M365_Login_Auth $auth, M365_Login_Graph $graph ) {
$this->settings = $settings; $this->settings = $settings;
$this->auth = $auth; $this->auth = $auth;
$this->graph = $graph;
add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_menu', array( $this, 'menu' ) );
add_action( 'admin_init', array( $this, 'register' ) ); add_action( 'admin_init', array( $this, 'register' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
add_action( 'wp_ajax_' . self::AJAX_TEST, array( $this, 'ajax_test_connection' ) ); add_action( 'wp_ajax_' . self::AJAX_TEST, array( $this, 'ajax_test_connection' ) );
add_action( 'wp_ajax_' . self::AJAX_GROUPS, array( $this, 'ajax_search_groups' ) );
add_action( 'update_option_' . M365_LOGIN_OPTION, array( $this->graph, 'flush_token' ) );
add_action( 'admin_notices', array( $this, 'setup_notice' ) ); add_action( 'admin_notices', array( $this, 'setup_notice' ) );
} }
@ -127,6 +139,7 @@ class M365_Login_Admin {
'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( self::NONCE_TEST ), 'nonce' => wp_create_nonce( self::NONCE_TEST ),
'action' => self::AJAX_TEST, 'action' => self::AJAX_TEST,
'groupAction' => self::AJAX_GROUPS,
'defaultLogo' => M365_Login_Button::microsoft_logo(), 'defaultLogo' => M365_Login_Button::microsoft_logo(),
'i18n' => array( 'i18n' => array(
'chooseIcon' => __( 'Choose button icon', 'm365-login' ), 'chooseIcon' => __( 'Choose button icon', 'm365-login' ),
@ -135,6 +148,12 @@ class M365_Login_Admin {
'copy' => __( 'Copy', 'm365-login' ), 'copy' => __( 'Copy', 'm365-login' ),
'testing' => __( 'Testing…', 'm365-login' ), 'testing' => __( 'Testing…', 'm365-login' ),
'testFailed' => __( 'The tenant could not be reached. Check the tenant ID and the servers outgoing connections.', 'm365-login' ), 'testFailed' => __( 'The tenant could not be reached. Check the tenant ID and the servers outgoing connections.', 'm365-login' ),
'noGroups' => __( 'No groups found.', 'm365-login' ),
'searching' => __( 'Searching…', 'm365-login' ),
'add' => __( 'Add', 'm365-login' ),
'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' ),
), ),
) )
); );
@ -179,6 +198,32 @@ class M365_Login_Admin {
); );
} }
/**
* AJAX: search Entra groups through Microsoft Graph.
*/
public function ajax_search_groups() {
check_ajax_referer( self::NONCE_TEST, 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'm365-login' ) ), 403 );
}
if ( ! $this->settings->is_configured() ) {
wp_send_json_error( array( 'message' => __( 'Save the connection settings first, then search for groups.', 'm365-login' ) ) );
}
$query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
$groups = $this->graph->search_groups( mb_substr( $query, 0, 100 ) );
if ( is_wp_error( $groups ) ) {
$message = $groups->get_error_message();
if ( false !== stripos( $message, 'Authorization_RequestDenied' ) || false !== stripos( $message, 'Insufficient privileges' ) ) {
$message = __( 'Microsoft Graph refused the request. Grant the application permission "GroupMember.Read.All" (or "Directory.Read.All") with admin consent in Entra ID.', 'm365-login' );
}
wp_send_json_error( array( 'message' => $message ) );
}
wp_send_json_success( array( 'groups' => $groups ) );
}
/** /**
* Renders the settings screen. * Renders the settings screen.
*/ */
@ -402,6 +447,79 @@ class M365_Login_Admin {
</div> </div>
</div> </div>
<div class="m365-card">
<h2 class="m365-card__title"><?php esc_html_e( 'Allowed Entra groups (optional)', 'm365-login' ); ?></h2>
<p class="m365-card__intro"><?php esc_html_e( 'Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count.', 'm365-login' ); ?></p>
<div class="m365-field">
<label for="m365-group-search"><?php esc_html_e( 'Search groups', 'm365-login' ); ?></label>
<div class="m365-field__row">
<input type="search" id="m365-group-search" class="regular-text" placeholder="<?php esc_attr_e( 'Type a group name or paste an object ID…', 'm365-login' ); ?>" autocomplete="off" <?php disabled( ! $configured ); ?> />
<button type="button" class="button" id="m365-group-search-btn" <?php disabled( ! $configured ); ?>><?php esc_html_e( 'Search', 'm365-login' ); ?></button>
</div>
<?php if ( ! $configured ) : ?>
<p class="description"><?php esc_html_e( 'Save the connection settings first, then search for groups.', 'm365-login' ); ?></p>
<?php else : ?>
<p class="description"><?php esc_html_e( 'Needs the application permission "GroupMember.Read.All" with admin consent. Without it you can still paste group object IDs.', 'm365-login' ); ?></p>
<?php endif; ?>
<div id="m365-group-results" class="m365-group-results" hidden></div>
</div>
<div class="m365-field">
<span class="m365-field__label"><?php esc_html_e( 'Selected groups', 'm365-login' ); ?></span>
<ul id="m365-group-list" class="m365-group-list" data-empty="<?php esc_attr_e( 'No groups selected every matched user may sign in.', 'm365-login' ); ?>">
<?php foreach ( $this->settings->allowed_groups() as $gid => $gname ) : ?>
<li class="m365-group-chip" data-id="<?php echo esc_attr( $gid ); ?>">
<span class="m365-group-chip__name"><?php echo esc_html( $gname ); ?></span>
<code class="m365-group-chip__id"><?php echo esc_html( $gid ); ?></code>
<input type="hidden" name="<?php echo esc_attr( $option . '[allowed_groups][' . $gid . ']' ); ?>" value="<?php echo esc_attr( $gname ); ?>" />
<button type="button" class="m365-group-chip__remove" aria-label="<?php esc_attr_e( 'Remove', 'm365-login' ); ?>">&times;</button>
</li>
<?php endforeach; ?>
</ul>
<p class="description"><?php esc_html_e( '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.', 'm365-login' ); ?></p>
</div>
</div>
<div class="m365-card">
<h2 class="m365-card__title"><?php esc_html_e( 'Button-only mode', 'm365-login' ); ?></h2>
<p class="m365-card__intro"><?php esc_html_e( 'Hide the username/password form and the "Lost your password?" link, and refuse password sign-ins on the login page. Application passwords, REST and XML-RPC are not affected.', 'm365-login' ); ?></p>
<label class="m365-check m365-check--block">
<input type="checkbox" name="<?php echo $field( 'button_only' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['button_only'] ); ?> id="m365-button-only" />
<span>
<strong><?php esc_html_e( 'Show only the Microsoft button on the login page', 'm365-login' ); ?></strong>
<em><?php esc_html_e( 'Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this.', 'm365-login' ); ?></em>
</span>
</label>
<div class="m365-fallback">
<span class="m365-field__label"><?php esc_html_e( 'Fallback link (keep it secret)', 'm365-login' ); ?></span>
<p class="description"><?php esc_html_e( 'Opening this link shows the password form again in that browser for 30 minutes. Bookmark it somewhere safe it is your way back in if Microsoft sign-in ever breaks.', 'm365-login' ); ?></p>
<?php if ( '' !== $this->settings->fallback_url() ) : ?>
<div class="m365-copy">
<code id="m365-fallback-url"><?php echo esc_html( $this->settings->fallback_url() ); ?></code>
<button type="button" class="button button-small m365-copy__button" data-copy="m365-fallback-url"><?php esc_html_e( 'Copy', 'm365-login' ); ?></button>
</div>
<label class="m365-check m365-check--inline">
<input type="checkbox" name="<?php echo $field( 'fallback_regenerate' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" id="m365-fallback-regenerate" />
<?php esc_html_e( 'Generate a new key when saving', 'm365-login' ); ?>
</label>
<?php else : ?>
<p class="m365-inline-result"><?php esc_html_e( 'A key is generated automatically the first time you save these settings.', 'm365-login' ); ?></p>
<?php endif; ?>
<p class="description">
<?php
printf(
/* translators: %s: PHP constant */
esc_html__( 'Emergency switch: add %s to wp-config.php to disable button-only mode entirely.', 'm365-login' ),
'<code>define( \'M365_LOGIN_DISABLE_BUTTON_ONLY\', true );</code>'
);
?>
</p>
</div>
</div>
<div class="m365-card m365-card--muted"> <div class="m365-card m365-card--muted">
<h2 class="m365-card__title"><?php esc_html_e( 'What the plugin does to keep sign-ins safe', 'm365-login' ); ?></h2> <h2 class="m365-card__title"><?php esc_html_e( 'What the plugin does to keep sign-ins safe', 'm365-login' ); ?></h2>
<ul class="m365-list"> <ul class="m365-list">
@ -444,6 +562,7 @@ class M365_Login_Admin {
<li><?php esc_html_e( 'Under Token configuration add the optional claim "email" for ID tokens (recommended), then save this page.', 'm365-login' ); ?></li> <li><?php esc_html_e( 'Under Token configuration add the optional claim "email" for ID tokens (recommended), then save this page.', 'm365-login' ); ?></li>
</ol> </ol>
<p class="description"><?php esc_html_e( 'Required API permission: openid, profile, email (delegated) granted by default.', 'm365-login' ); ?></p> <p class="description"><?php esc_html_e( 'Required API permission: openid, profile, email (delegated) granted by default.', 'm365-login' ); ?></p>
<p class="description"><?php esc_html_e( 'Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent.', 'm365-login' ); ?></p>
</div> </div>
<div class="m365-card m365-card--muted"> <div class="m365-card m365-card--muted">

View file

@ -15,6 +15,8 @@ class M365_Login_Auth {
const ACTION_START = 'm365_login'; const ACTION_START = 'm365_login';
const CALLBACK_PATH = 'm365-login/callback'; const CALLBACK_PATH = 'm365-login/callback';
const STATE_COOKIE = 'm365_login_state'; const STATE_COOKIE = 'm365_login_state';
const FALLBACK_COOKIE = 'm365_login_fallback';
const FALLBACK_TTL = 30 * MINUTE_IN_SECONDS;
const STATE_TTL = 600; // 10 minutes. const STATE_TTL = 600; // 10 minutes.
const META_OID = '_m365_login_oid'; const META_OID = '_m365_login_oid';
const META_LAST_LOGIN = '_m365_login_last_login'; const META_LAST_LOGIN = '_m365_login_last_login';
@ -28,17 +30,123 @@ class M365_Login_Auth {
*/ */
private $settings; private $settings;
/**
* Graph client.
*
* @var M365_Login_Graph
*/
private $graph;
/** /**
* Constructor. * Constructor.
* *
* @param M365_Login_Settings $settings Settings. * @param M365_Login_Settings $settings Settings.
* @param M365_Login_Graph $graph Graph client.
*/ */
public function __construct( M365_Login_Settings $settings ) { public function __construct( M365_Login_Settings $settings, M365_Login_Graph $graph ) {
$this->settings = $settings; $this->settings = $settings;
$this->graph = $graph;
add_action( 'login_form_' . self::ACTION_START, array( $this, 'handle_start' ) ); add_action( 'login_form_' . self::ACTION_START, array( $this, 'handle_start' ) );
add_action( 'init', array( $this, 'maybe_handle_callback' ), 5 ); add_action( 'init', array( $this, 'maybe_handle_callback' ), 5 );
add_filter( 'wp_login_errors', array( $this, 'login_errors' ), 10, 1 ); add_filter( 'wp_login_errors', array( $this, 'login_errors' ), 10, 1 );
// Button-only mode.
add_action( 'login_init', array( $this, 'maybe_accept_fallback_key' ) );
// Runs after core's username/password handlers (priority 20), which would otherwise overwrite an early WP_Error.
add_filter( 'authenticate', array( $this, 'block_password_login' ), 99, 3 );
}
/* ------------------------------------------------------------------ */
/* Button-only mode */
/* ------------------------------------------------------------------ */
/**
* Whether the current browser presented the fallback key (cookie set for 30 minutes).
*
* @return bool
*/
public function fallback_active() {
if ( ! $this->settings->button_only() ) {
return true; // Nothing is hidden, the form is always available.
}
$cookie = isset( $_COOKIE[ self::FALLBACK_COOKIE ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::FALLBACK_COOKIE ] ) ) : '';
return '' !== $cookie && hash_equals( $this->fallback_cookie_value(), $cookie );
}
/**
* Expected fallback cookie value (HMAC of the key, so the key itself never sits in the cookie).
*
* @return string
*/
private function fallback_cookie_value() {
return hash_hmac( 'sha256', 'fallback|' . $this->settings->fallback_key(), wp_salt( 'auth' ) );
}
/**
* wp-login.php?m365_fallback=KEY sets the fallback cookie and reloads without the key in the URL.
*/
public function maybe_accept_fallback_key() {
if ( ! $this->settings->button_only() ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the key itself is the secret.
$given = isset( $_GET['m365_fallback'] ) ? sanitize_text_field( wp_unslash( $_GET['m365_fallback'] ) ) : '';
if ( '' === $given ) {
return;
}
// Slow down brute force attempts on the key.
$ip_key = 'm365_login_fb_' . md5( $this->client_ip() );
$attempts = (int) get_transient( $ip_key );
if ( $attempts >= 10 ) {
$this->fail( 'fallback_locked' );
}
if ( ! hash_equals( $this->settings->fallback_key(), $given ) ) {
set_transient( $ip_key, $attempts + 1, 15 * MINUTE_IN_SECONDS );
$this->fail( 'fallback_invalid' );
}
delete_transient( $ip_key );
$this->send_cookie( self::FALLBACK_COOKIE, $this->fallback_cookie_value(), time() + self::FALLBACK_TTL );
nocache_headers();
wp_safe_redirect( add_query_arg( 'm365_fallback', 'on', wp_login_url() ) );
exit;
}
/**
* Refuses username/password sign-in on wp-login.php while button-only mode is active.
*
* @param null|WP_User|WP_Error $user Result so far.
* @param string $username Username.
* @param string $password Password.
* @return null|WP_User|WP_Error
*/
public function block_password_login( $user, $username, $password ) {
if ( ! $this->settings->button_only() || $this->fallback_active() ) {
return $user;
}
if ( '' === (string) $username && '' === (string) $password ) {
return $user; // Initial form render or cookie auth, no password attempt.
}
// Only the interactive login form is affected: XML-RPC, REST and application passwords keep working.
if ( ! isset( $GLOBALS['pagenow'] ) || 'wp-login.php' !== $GLOBALS['pagenow'] ) {
return $user;
}
if ( ! $user instanceof WP_User ) {
return $user; // Already failed for another reason; keep core's message.
}
return new WP_Error( 'm365_login_button_only', __( 'Password sign-in is disabled on this site. Please use the Microsoft button.', 'm365-login' ) );
}
/**
* Best-effort client IP for rate limiting.
*
* @return string
*/
private function client_ip() {
return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '0.0.0.0';
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@ -276,8 +384,15 @@ class M365_Login_Auth {
$this->fail( 'no_user' ); $this->fail( 'no_user' );
} }
// Bind the account to the immutable Microsoft object ID after first login.
$oid = isset( $claims['oid'] ) && is_string( $claims['oid'] ) ? strtolower( $claims['oid'] ) : ''; $oid = isset( $claims['oid'] ) && is_string( $claims['oid'] ) ? strtolower( $claims['oid'] ) : '';
// Entra group restriction.
$group_check = $this->check_groups( $claims, $oid );
if ( true !== $group_check ) {
$this->fail( $group_check );
}
// Bind the account to the immutable Microsoft object ID after first login.
if ( $this->settings->get( 'bind_oid' ) ) { if ( $this->settings->get( 'bind_oid' ) ) {
if ( '' === $oid || ! M365_Login_Settings::is_guid( $oid ) ) { if ( '' === $oid || ! M365_Login_Settings::is_guid( $oid ) ) {
$this->fail( 'invalid_token' ); $this->fail( 'invalid_token' );
@ -459,6 +574,49 @@ class M365_Login_Auth {
return $body; return $body;
} }
/**
* Verifies membership in one of the allowed Entra groups.
*
* Uses the "groups" claim when the token carries one (and is not in overage),
* otherwise asks Microsoft Graph (transitive check, needs application permissions).
*
* @param array $claims Verified claims.
* @param string $oid User object ID.
* @return true|string True, or an error code for fail().
*/
private function check_groups( $claims, $oid ) {
$allowed = array_keys( $this->settings->allowed_groups() );
if ( empty( $allowed ) ) {
return true;
}
$overage = ! empty( $claims['_claim_names'] ) || ! empty( $claims['hasgroups'] );
if ( ! $overage && isset( $claims['groups'] ) && is_array( $claims['groups'] ) ) {
$token_groups = array_map( 'strtolower', array_filter( $claims['groups'], 'is_string' ) );
if ( array_intersect( $allowed, $token_groups ) ) {
return true;
}
// The claim is authoritative when present: no need to ask Graph.
$this->log( 'User is not a member of an allowed group (token claim).' );
return 'not_in_group';
}
if ( '' === $oid || ! M365_Login_Settings::is_guid( $oid ) ) {
return 'invalid_token';
}
$matches = $this->graph->check_member_groups( $oid, $allowed );
if ( is_wp_error( $matches ) ) {
$this->log( 'Group check via Microsoft Graph failed: ' . $matches->get_error_message() );
return 'group_check_failed';
}
if ( empty( $matches ) ) {
$this->log( 'User is not a member of an allowed group (Graph).' );
return 'not_in_group';
}
return true;
}
/** /**
* Extracts the e-mail address used for matching. * Extracts the e-mail address used for matching.
* *
@ -520,28 +678,29 @@ class M365_Login_Auth {
* @param string $token Cookie value. * @param string $token Cookie value.
*/ */
private function set_state_cookie( $token ) { private function set_state_cookie( $token ) {
$this->send_cookie( $token, time() + self::STATE_TTL ); $this->send_cookie( self::STATE_COOKIE, $token, time() + self::STATE_TTL );
} }
/** /**
* Removes the state cookie. * Removes the state cookie.
*/ */
private function clear_state_cookie() { private function clear_state_cookie() {
$this->send_cookie( '', time() - YEAR_IN_SECONDS ); $this->send_cookie( self::STATE_COOKIE, '', time() - YEAR_IN_SECONDS );
} }
/** /**
* Cookie helper: HttpOnly, SameSite=Lax (needed for the top-level redirect back), Secure on HTTPS. * Cookie helper: HttpOnly, SameSite=Lax (needed for the top-level redirect back), Secure on HTTPS.
* *
* @param string $name Cookie name.
* @param string $value Value. * @param string $value Value.
* @param int $expires Expiry timestamp. * @param int $expires Expiry timestamp.
*/ */
private function send_cookie( $value, $expires ) { private function send_cookie( $name, $value, $expires ) {
$path = wp_parse_url( home_url( '/' ), PHP_URL_PATH ); $path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
$path = is_string( $path ) && '' !== $path ? $path : '/'; $path = is_string( $path ) && '' !== $path ? $path : '/';
setcookie( setcookie(
self::STATE_COOKIE, $name,
$value, $value,
array( array(
'expires' => $expires, 'expires' => $expires,
@ -583,8 +742,18 @@ class M365_Login_Auth {
* @return WP_Error * @return WP_Error
*/ */
public function login_errors( $errors ) { public function login_errors( $errors ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display of a whitelisted error code. if ( ! $errors instanceof WP_Error ) {
$code = isset( $_GET['m365_error'] ) ? sanitize_key( wp_unslash( $_GET['m365_error'] ) ) : ''; $errors = new WP_Error();
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only display of whitelisted flags.
$code = isset( $_GET['m365_error'] ) ? sanitize_key( wp_unslash( $_GET['m365_error'] ) ) : '';
$fallback_on = isset( $_GET['m365_fallback'] ) && 'on' === $_GET['m365_fallback'];
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( $fallback_on && $this->settings->button_only() && $this->fallback_active() ) {
$errors->add( 'm365_login_fallback_on', __( 'Password sign-in is temporarily enabled for this browser (30 minutes).', 'm365-login' ), 'message' );
}
if ( '' === $code ) { if ( '' === $code ) {
return $errors; return $errors;
} }
@ -601,11 +770,12 @@ class M365_Login_Auth {
'no_user' => __( 'No WordPress account exists for your Microsoft e-mail address.', 'm365-login' ), 'no_user' => __( 'No WordPress account exists for your Microsoft e-mail address.', 'm365-login' ),
'oid_mismatch' => __( 'This WordPress account is linked to a different Microsoft account. Please contact an administrator.', 'm365-login' ), 'oid_mismatch' => __( 'This WordPress account is linked to a different Microsoft account. Please contact an administrator.', 'm365-login' ),
'not_allowed' => __( 'You are not allowed to sign in with this account.', 'm365-login' ), 'not_allowed' => __( 'You are not allowed to sign in with this account.', 'm365-login' ),
'not_in_group' => __( 'Your Microsoft account is not a member of a group that is allowed to sign in here.', 'm365-login' ),
'group_check_failed' => __( 'Your group membership could not be verified. Please contact an administrator.', 'm365-login' ),
'fallback_invalid' => __( 'The fallback key is not valid.', 'm365-login' ),
'fallback_locked' => __( 'Too many attempts. Please wait 15 minutes.', 'm365-login' ),
); );
if ( ! $errors instanceof WP_Error ) {
$errors = new WP_Error();
}
$errors->add( $errors->add(
'm365_login_' . $code, 'm365_login_' . $code,
isset( $messages[ $code ] ) ? $messages[ $code ] : $messages['provider_error'], isset( $messages[ $code ] ) ? $messages[ $code ] : $messages['provider_error'],

View file

@ -28,6 +28,7 @@ class M365_Login_Button {
$this->settings = $settings; $this->settings = $settings;
add_action( 'login_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'login_enqueue_scripts', array( $this, 'enqueue' ) );
add_filter( 'login_body_class', array( $this, 'body_class' ) );
add_filter( 'login_message', array( $this, 'render_above' ), 20 ); add_filter( 'login_message', array( $this, 'render_above' ), 20 );
add_action( 'login_footer', array( $this, 'render_below' ) ); add_action( 'login_footer', array( $this, 'render_below' ) );
add_shortcode( 'm365_login_button', array( $this, 'shortcode' ) ); add_shortcode( 'm365_login_button', array( $this, 'shortcode' ) );
@ -57,6 +58,28 @@ class M365_Login_Button {
return (bool) apply_filters( 'm365_login_show_button', true ); return (bool) apply_filters( 'm365_login_show_button', true );
} }
/**
* Whether the password form is hidden for this request.
*
* @return bool
*/
private function form_hidden() {
return $this->should_render() && $this->settings->button_only() && ! M365_Login::instance()->auth->fallback_active();
}
/**
* Adds a body class while the password form is hidden.
*
* @param string[] $classes Body classes.
* @return string[]
*/
public function body_class( $classes ) {
if ( $this->form_hidden() ) {
$classes[] = 'm365-button-only';
}
return $classes;
}
/** /**
* Enqueues login styles and the small positioning script. * Enqueues login styles and the small positioning script.
*/ */
@ -140,7 +163,7 @@ class M365_Login_Button {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- passed through to the flow, validated there. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- passed through to the flow, validated there.
$redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : ''; $redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : '';
$divider = (string) $this->settings->get( 'divider_text' ); $divider = $this->form_hidden() ? '' : (string) $this->settings->get( 'divider_text' );
$divider = '' === trim( $divider ) ? '' : '<div class="m365-login__divider" aria-hidden="true"><span>' . esc_html( $divider ) . '</span></div>'; $divider = '' === trim( $divider ) ? '' : '<div class="m365-login__divider" aria-hidden="true"><span>' . esc_html( $divider ) . '</span></div>';
$html = '<div class="m365-login m365-login--' . esc_attr( $position ) . '" id="m365-login-block">'; $html = '<div class="m365-login m365-login--' . esc_attr( $position ) . '" id="m365-login-block">';

View file

@ -0,0 +1,240 @@
<?php
/**
* Minimal Microsoft Graph client (application permissions).
*
* @package M365_Login
*/
defined( 'ABSPATH' ) || exit;
/**
* Obtains app-only tokens via client credentials and queries groups.
*/
class M365_Login_Graph {
const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
const TOKEN_TTL = 50 * MINUTE_IN_SECONDS; // Graph tokens last ~60 minutes.
const HTTP_TIMEOUT = 15;
/**
* Settings.
*
* @var M365_Login_Settings
*/
private $settings;
/**
* Constructor.
*
* @param M365_Login_Settings $settings Settings.
*/
public function __construct( M365_Login_Settings $settings ) {
$this->settings = $settings;
}
/**
* Transient key for the cached app token.
*
* @return string
*/
private function token_cache_key() {
return 'm365_login_apptoken_' . md5( $this->settings->tenant() . '|' . $this->settings->get( 'client_id' ) );
}
/**
* Drops the cached app token (e.g. after the client secret changed).
*/
public function flush_token() {
delete_transient( $this->token_cache_key() );
}
/**
* Returns an app-only access token for Microsoft Graph.
*
* @return string|WP_Error
*/
public function app_token() {
$cached = get_transient( $this->token_cache_key() );
if ( is_string( $cached ) && '' !== $cached ) {
return $cached;
}
if ( ! $this->settings->is_configured() ) {
return new WP_Error( 'graph_not_configured', __( 'Microsoft login is not configured yet.', 'm365-login' ) );
}
$response = wp_remote_post(
'https://login.microsoftonline.com/' . rawurlencode( $this->settings->tenant() ) . '/oauth2/v2.0/token',
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',
),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) || empty( $body['access_token'] ) ) {
$detail = is_array( $body ) && ! empty( $body['error_description'] ) ? (string) $body['error_description'] : 'HTTP ' . wp_remote_retrieve_response_code( $response );
return new WP_Error( 'graph_token', $detail );
}
set_transient( $this->token_cache_key(), (string) $body['access_token'], self::TOKEN_TTL );
return (string) $body['access_token'];
}
/**
* Performs an authenticated Graph request.
*
* @param string $method HTTP method.
* @param string $path Path relative to the v1.0 base (with query string).
* @param array|null $json JSON body for POST requests.
* @param array $headers Extra headers.
* @return array|WP_Error Decoded JSON.
*/
private function request( $method, $path, $json = null, $headers = array() ) {
$token = $this->app_token();
if ( is_wp_error( $token ) ) {
return $token;
}
$args = array(
'method' => $method,
'timeout' => self::HTTP_TIMEOUT,
'headers' => array_merge(
array(
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
),
$headers
),
);
if ( null !== $json ) {
$args['headers']['Content-Type'] = 'application/json';
$args['body'] = wp_json_encode( $json );
}
$response = wp_remote_request( self::GRAPH_BASE . $path, $args );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = (int) wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( 401 === $code ) {
$this->flush_token();
}
if ( $code < 200 || $code >= 300 || ! is_array( $body ) ) {
$graph_code = isset( $body['error']['code'] ) ? (string) $body['error']['code'] : 'HTTP ' . $code;
$message = isset( $body['error']['message'] ) ? (string) $body['error']['message'] : '';
return new WP_Error( 'graph_' . sanitize_key( $graph_code ), $graph_code . ( $message ? ': ' . $message : '' ) );
}
return $body;
}
/**
* Searches groups by display name.
*
* @param string $query Search text (may be empty for the first page).
* @return array|WP_Error List of ['id' => .., 'name' => .., 'description' => ..].
*/
public function search_groups( $query ) {
$query = trim( (string) $query );
$select = '$select=id,displayName,description,securityEnabled,mailEnabled&$top=25&$orderby=displayName';
if ( '' !== $query && M365_Login_Settings::is_guid( $query ) ) {
$path = '/groups/' . rawurlencode( strtolower( $query ) ) . '?$select=id,displayName,description,securityEnabled,mailEnabled';
$item = $this->request( 'GET', $path );
if ( is_wp_error( $item ) ) {
return $item;
}
return array( $this->format_group( $item ) );
}
$path = '/groups?' . $select;
if ( '' !== $query ) {
// $search needs the ConsistencyLevel header; the value must be wrapped in double quotes.
$search = '"displayName:' . str_replace( '"', '', $query ) . '"';
$path = '/groups?' . $select . '&$search=' . rawurlencode( $search ) . '&$count=true';
}
$result = $this->request( 'GET', $path, null, array( 'ConsistencyLevel' => 'eventual' ) );
if ( is_wp_error( $result ) ) {
return $result;
}
$groups = array();
if ( ! empty( $result['value'] ) && is_array( $result['value'] ) ) {
foreach ( $result['value'] as $item ) {
if ( is_array( $item ) && ! empty( $item['id'] ) ) {
$groups[] = $this->format_group( $item );
}
}
}
return $groups;
}
/**
* Normalises a Graph group object.
*
* @param array $item Graph group.
* @return array
*/
private function format_group( $item ) {
$type = __( 'Group', 'm365-login' );
if ( ! empty( $item['securityEnabled'] ) && empty( $item['mailEnabled'] ) ) {
$type = __( 'Security group', 'm365-login' );
} elseif ( ! empty( $item['mailEnabled'] ) ) {
$type = __( 'Microsoft 365 group', 'm365-login' );
}
return array(
'id' => strtolower( (string) $item['id'] ),
'name' => isset( $item['displayName'] ) ? (string) $item['displayName'] : (string) $item['id'],
'description' => isset( $item['description'] ) ? (string) $item['description'] : '',
'type' => $type,
);
}
/**
* Checks (transitively) which of the given groups the user belongs to.
*
* @param string $user_oid User object ID.
* @param string[] $group_ids Group object IDs (any count; chunked by 20).
* @return string[]|WP_Error Matching group IDs.
*/
public function check_member_groups( $user_oid, $group_ids ) {
if ( ! M365_Login_Settings::is_guid( $user_oid ) ) {
return new WP_Error( 'graph_bad_oid', 'Invalid user object ID.' );
}
$matches = array();
foreach ( array_chunk( array_values( $group_ids ), 20 ) as $chunk ) {
$result = $this->request(
'POST',
'/users/' . rawurlencode( strtolower( $user_oid ) ) . '/checkMemberGroups',
array( 'groupIds' => $chunk )
);
if ( is_wp_error( $result ) ) {
return $result;
}
if ( ! empty( $result['value'] ) && is_array( $result['value'] ) ) {
foreach ( $result['value'] as $id ) {
$matches[] = strtolower( (string) $id );
}
}
if ( ! empty( $matches ) ) {
break; // One match is enough.
}
}
return $matches;
}
}

View file

@ -35,7 +35,11 @@ class M365_Login_Settings {
'upn_fallback' => 1, 'upn_fallback' => 1,
'bind_oid' => 1, 'bind_oid' => 1,
'allowed_domains' => '', 'allowed_domains' => '',
'allowed_groups' => array(), // id => display name.
'remember_me' => 0, 'remember_me' => 0,
// Button-only mode.
'button_only' => 0,
'fallback_key' => '',
// Button appearance. // Button appearance.
'button_text' => __( 'Sign in with Microsoft', 'm365-login' ), 'button_text' => __( 'Sign in with Microsoft', 'm365-login' ),
'button_icon' => '', // Empty = bundled Microsoft logo. 'button_icon' => '', // Empty = bundled Microsoft logo.
@ -157,6 +161,71 @@ class M365_Login_Settings {
return array_values( array_unique( $out ) ); return array_values( array_unique( $out ) );
} }
/**
* Allowed Entra group IDs (lowercase GUIDs) mapped to display names.
*
* @return array
*/
public function allowed_groups() {
$raw = $this->get( 'allowed_groups', array() );
$out = array();
if ( is_array( $raw ) ) {
foreach ( $raw as $id => $name ) {
$id = strtolower( (string) $id );
if ( self::is_guid( $id ) ) {
$out[ $id ] = (string) $name;
}
}
}
return $out;
}
/**
* Whether the password form is hidden and password sign-in blocked.
*
* @return bool
*/
public function button_only() {
if ( defined( 'M365_LOGIN_DISABLE_BUTTON_ONLY' ) && M365_LOGIN_DISABLE_BUTTON_ONLY ) {
return false;
}
return $this->is_configured() && (bool) $this->get( 'button_only' ) && '' !== $this->fallback_key();
}
/**
* Secret key that re-enables the password form.
*
* @return string
*/
public function fallback_key() {
$key = (string) $this->get( 'fallback_key', '' );
return preg_match( '/^[A-Za-z0-9]{16,64}$/', $key ) ? $key : '';
}
/**
* URL that shows the password form again when button-only mode is active.
*
* @return string
*/
public function fallback_url() {
$key = $this->fallback_key();
return '' === $key ? '' : add_query_arg( 'm365_fallback', $key, wp_login_url() );
}
/**
* Generates a new fallback key.
*
* @return string
*/
public static function generate_fallback_key() {
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
$key = '';
for ( $i = 0; $i < 24; $i++ ) {
$key .= $alphabet[ random_int( 0, strlen( $alphabet ) - 1 ) ];
}
return $key;
}
/** /**
* Sanitises settings coming from the admin form. * Sanitises settings coming from the admin form.
* *
@ -215,6 +284,31 @@ class M365_Login_Settings {
$domains = preg_replace( '/[^a-z0-9.\-@,;\s]/i', '', $domains ); $domains = preg_replace( '/[^a-z0-9.\-@,;\s]/i', '', $domains );
$out['allowed_domains'] = trim( (string) $domains ); $out['allowed_domains'] = trim( (string) $domains );
// Allowed groups: GUID => name.
$groups = array();
if ( ! empty( $input['allowed_groups'] ) && is_array( $input['allowed_groups'] ) ) {
foreach ( $input['allowed_groups'] as $id => $name ) {
$id = strtolower( trim( sanitize_text_field( wp_unslash( (string) $id ) ) ) );
if ( ! self::is_guid( $id ) ) {
continue;
}
$name = sanitize_text_field( wp_unslash( (string) $name ) );
$groups[ $id ] = '' === $name ? $id : mb_substr( $name, 0, 120 );
if ( count( $groups ) >= 100 ) {
break;
}
}
}
$out['allowed_groups'] = $groups;
// Button-only mode + fallback key.
$out['button_only'] = empty( $input['button_only'] ) ? 0 : 1;
$key = (string) $current['fallback_key'];
if ( ! empty( $input['fallback_regenerate'] ) || ! preg_match( '/^[A-Za-z0-9]{16,64}$/', $key ) ) {
$key = self::generate_fallback_key();
}
$out['fallback_key'] = $key;
// Button. // Button.
$text = isset( $input['button_text'] ) ? sanitize_text_field( wp_unslash( $input['button_text'] ) ) : ''; $text = isset( $input['button_text'] ) ? sanitize_text_field( wp_unslash( $input['button_text'] ) ) : '';
$out['button_text'] = '' === trim( $text ) ? $defaults['button_text'] : mb_substr( $text, 0, 80 ); $out['button_text'] = '' === trim( $text ) ? $defaults['button_text'] : mb_substr( $text, 0, 80 );

View file

@ -33,6 +33,13 @@ final class M365_Login {
*/ */
public $auth; public $auth;
/**
* Microsoft Graph client.
*
* @var M365_Login_Graph
*/
public $graph;
/** /**
* Login button component. * Login button component.
* *
@ -66,11 +73,12 @@ final class M365_Login {
add_action( 'init', array( $this, 'load_textdomain' ) ); add_action( 'init', array( $this, 'load_textdomain' ) );
$this->settings = new M365_Login_Settings(); $this->settings = new M365_Login_Settings();
$this->auth = new M365_Login_Auth( $this->settings ); $this->graph = new M365_Login_Graph( $this->settings );
$this->auth = new M365_Login_Auth( $this->settings, $this->graph );
$this->button = new M365_Login_Button( $this->settings ); $this->button = new M365_Login_Button( $this->settings );
if ( is_admin() ) { if ( is_admin() ) {
$this->admin = new M365_Login_Admin( $this->settings, $this->auth ); $this->admin = new M365_Login_Admin( $this->settings, $this->auth, $this->graph );
} }
add_filter( 'plugin_action_links_' . plugin_basename( M365_LOGIN_FILE ), array( $this, 'action_links' ) ); add_filter( 'plugin_action_links_' . plugin_basename( M365_LOGIN_FILE ), array( $this, 'action_links' ) );

Binary file not shown.

View file

@ -16,445 +16,587 @@ msgstr ""
"X-Generator: bin/make-pot.py\n" "X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n" "X-Domain: m365-login\n"
#: includes/class-m365-login-admin.php:63 includes/class-m365-login-admin.php:64 includes/class-m365-login-admin.php:203 #: includes/class-m365-login-admin.php:75 includes/class-m365-login-admin.php:76 includes/class-m365-login-admin.php:248
msgid "M365 Login" msgid "M365 Login"
msgstr "M365 Login" msgstr "M365 Login"
#: includes/class-m365-login-admin.php:102 #: includes/class-m365-login-admin.php:114
msgid "M365 Login is active but not connected to Microsoft Entra ID yet." 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." msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
#: includes/class-m365-login-admin.php:104 #: includes/class-m365-login-admin.php:116
msgid "Open the settings" msgid "Open the settings"
msgstr "Einstellungen öffnen" msgstr "Einstellungen öffnen"
#: includes/class-m365-login-admin.php:132 #: includes/class-m365-login-admin.php:145
msgid "Choose button icon" msgid "Choose button icon"
msgstr "Button-Icon auswählen" msgstr "Button-Icon auswählen"
#: includes/class-m365-login-admin.php:133 #: includes/class-m365-login-admin.php:146
msgid "Use this icon" msgid "Use this icon"
msgstr "Dieses Icon verwenden" msgstr "Dieses Icon verwenden"
#: includes/class-m365-login-admin.php:134 #: includes/class-m365-login-admin.php:147
msgid "Copied!" msgid "Copied!"
msgstr "Kopiert!" msgstr "Kopiert!"
#: includes/class-m365-login-admin.php:135 includes/class-m365-login-admin.php:427 #: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:502 includes/class-m365-login-admin.php:545
msgid "Copy" msgid "Copy"
msgstr "Kopieren" msgstr "Kopieren"
#: includes/class-m365-login-admin.php:136 #: includes/class-m365-login-admin.php:149
msgid "Testing…" msgid "Testing…"
msgstr "Wird geprüft …" msgstr "Wird geprüft …"
#: includes/class-m365-login-admin.php:137 #: includes/class-m365-login-admin.php:150
msgid "The tenant could not be reached. Check the tenant ID and the servers outgoing connections." msgid "The tenant could not be reached. Check the tenant ID and the servers outgoing connections."
msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen." msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen."
#: includes/class-m365-login-admin.php:149 #: includes/class-m365-login-admin.php:151
msgid "No groups found."
msgstr "Keine Gruppen gefunden."
#: includes/class-m365-login-admin.php:152
msgid "Searching…"
msgstr "Suche läuft …"
#: includes/class-m365-login-admin.php:153
msgid "Add"
msgstr "Hinzufügen"
#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:476
msgid "Remove"
msgstr "Entfernen"
#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:461
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
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
msgid "You are not allowed to do this." msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung." msgstr "Dafür fehlt die Berechtigung."
#: includes/class-m365-login-admin.php:154 #: includes/class-m365-login-admin.php:173
msgid "Please enter a valid tenant ID first." msgid "Please enter a valid tenant ID first."
msgstr "Bitte zuerst eine gültige Tenant-ID eingeben." msgstr "Bitte zuerst eine gültige Tenant-ID eingeben."
#. translators: %d: HTTP status code #. translators: %d: HTTP status code
#: includes/class-m365-login-admin.php:168 #: includes/class-m365-login-admin.php:187
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?" msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?" msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?"
#. translators: %d: HTTP status code #. translators: %d: HTTP status code
#: includes/class-m365-login-admin.php:177 #: includes/class-m365-login-admin.php:196
msgid "Tenant reachable. The OpenID configuration was loaded successfully." msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen." msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen."
#: includes/class-m365-login-admin.php:187 #: includes/class-m365-login-admin.php:219
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
msgid "You are not allowed to access this page." msgid "You are not allowed to access this page."
msgstr "Für diese Seite fehlt die Berechtigung." msgstr "Für diese Seite fehlt die Berechtigung."
#: includes/class-m365-login-admin.php:204 #: includes/class-m365-login-admin.php:249
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account." 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." msgstr "Bestehende Benutzer melden sich mit ihrem Microsoft 365 / Entra ID-Konto an."
#: includes/class-m365-login-admin.php:209 #: includes/class-m365-login-admin.php:254
msgid "Connected" msgid "Connected"
msgstr "Verbunden" msgstr "Verbunden"
#: includes/class-m365-login-admin.php:209 #: includes/class-m365-login-admin.php:254
msgid "Setup incomplete" msgid "Setup incomplete"
msgstr "Einrichtung unvollständig" msgstr "Einrichtung unvollständig"
#: includes/class-m365-login-admin.php:217 #: includes/class-m365-login-admin.php:262
msgid "Connection" msgid "Connection"
msgstr "Verbindung" msgstr "Verbindung"
#: includes/class-m365-login-admin.php:218 #: includes/class-m365-login-admin.php:263
msgid "Button" msgid "Button"
msgstr "Button" msgstr "Button"
#: includes/class-m365-login-admin.php:219 #: includes/class-m365-login-admin.php:264
msgid "Security" msgid "Security"
msgstr "Sicherheit" msgstr "Sicherheit"
#: includes/class-m365-login-admin.php:228 #: includes/class-m365-login-admin.php:273
msgid "Microsoft Entra ID app registration" msgid "Microsoft Entra ID app registration"
msgstr "App-Registrierung in Microsoft Entra ID" msgstr "App-Registrierung in Microsoft Entra ID"
#: includes/class-m365-login-admin.php:229 #: includes/class-m365-login-admin.php:274
msgid "Enter the values from your app registration in the Microsoft Entra admin center." 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." msgstr "Trage hier die Werte aus deiner App-Registrierung im Microsoft Entra Admin Center ein."
#: includes/class-m365-login-admin.php:232 #: includes/class-m365-login-admin.php:277
msgid "Directory (tenant) ID" msgid "Directory (tenant) ID"
msgstr "Verzeichnis-ID (Mandant/Tenant)" msgstr "Verzeichnis-ID (Mandant/Tenant)"
#: includes/class-m365-login-admin.php:235 #: includes/class-m365-login-admin.php:280
msgid "Test tenant" msgid "Test tenant"
msgstr "Tenant testen" msgstr "Tenant testen"
#: includes/class-m365-login-admin.php:237 #: includes/class-m365-login-admin.php:282
msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account." 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." 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:242 #: includes/class-m365-login-admin.php:287
msgid "Application (client) ID" msgid "Application (client) ID"
msgstr "Anwendungs-ID (Client)" msgstr "Anwendungs-ID (Client)"
#: includes/class-m365-login-admin.php:247 #: includes/class-m365-login-admin.php:292
msgid "Client secret" msgid "Client secret"
msgstr "Geheimer Clientschlüssel (Client Secret)" msgstr "Geheimer Clientschlüssel (Client Secret)"
#: includes/class-m365-login-admin.php:249 #: includes/class-m365-login-admin.php:294
msgid "•••••••••••• (stored, leave empty to keep)" msgid "•••••••••••• (stored, leave empty to keep)"
msgstr "•••••••••••• (gespeichert leer lassen, um zu behalten)" msgstr "•••••••••••• (gespeichert leer lassen, um zu behalten)"
#: includes/class-m365-login-admin.php:249 #: includes/class-m365-login-admin.php:294
msgid "Paste the secret value" msgid "Paste the secret value"
msgstr "Wert des Secrets einfügen" msgstr "Wert des Secrets einfügen"
#: includes/class-m365-login-admin.php:250 #: includes/class-m365-login-admin.php:295
msgid "Show secret" msgid "Show secret"
msgstr "Secret anzeigen" msgstr "Secret anzeigen"
#: includes/class-m365-login-admin.php:255 #: includes/class-m365-login-admin.php:300
msgid "Remove the stored secret" msgid "Remove the stored secret"
msgstr "Gespeichertes Secret entfernen" msgstr "Gespeichertes Secret entfernen"
#: includes/class-m365-login-admin.php:258 #: 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." 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." 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:262 #: includes/class-m365-login-admin.php:307
msgid "Account prompt" msgid "Account prompt"
msgstr "Kontoauswahl" msgstr "Kontoauswahl"
#: includes/class-m365-login-admin.php:264 #: includes/class-m365-login-admin.php:309
msgid "Always let the user pick an account (recommended)" msgid "Always let the user pick an account (recommended)"
msgstr "Benutzer wählt immer ein Konto aus (empfohlen)" msgstr "Benutzer wählt immer ein Konto aus (empfohlen)"
#: includes/class-m365-login-admin.php:265 #: includes/class-m365-login-admin.php:310
msgid "Use the current Microsoft session if available" msgid "Use the current Microsoft session if available"
msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden" msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden"
#: includes/class-m365-login-admin.php:266 #: includes/class-m365-login-admin.php:311
msgid "Always require re-entering credentials" msgid "Always require re-entering credentials"
msgstr "Immer erneute Eingabe der Anmeldedaten verlangen" msgstr "Immer erneute Eingabe der Anmeldedaten verlangen"
#: includes/class-m365-login-admin.php:275 #: includes/class-m365-login-admin.php:320
msgid "Appearance" msgid "Appearance"
msgstr "Darstellung" msgstr "Darstellung"
#: includes/class-m365-login-admin.php:278 #: includes/class-m365-login-admin.php:323
msgid "Live preview" msgid "Live preview"
msgstr "Live-Vorschau" msgstr "Live-Vorschau"
#: includes/class-m365-login-admin.php:292 #: includes/class-m365-login-admin.php:337
msgid "Button text" msgid "Button text"
msgstr "Button-Text" msgstr "Button-Text"
#: includes/class-m365-login-admin.php:296 #: includes/class-m365-login-admin.php:341
msgid "Divider text" msgid "Divider text"
msgstr "Trennlinien-Text" msgstr "Trennlinien-Text"
#: includes/class-m365-login-admin.php:298 #: includes/class-m365-login-admin.php:343
msgid "Leave empty to hide the divider line." msgid "Leave empty to hide the divider line."
msgstr "Leer lassen, um die Trennlinie auszublenden." msgstr "Leer lassen, um die Trennlinie auszublenden."
#: includes/class-m365-login-admin.php:303 #: includes/class-m365-login-admin.php:348
msgid "Icon" msgid "Icon"
msgstr "Icon" msgstr "Icon"
#: includes/class-m365-login-admin.php:306 #: includes/class-m365-login-admin.php:351
msgid "Show an icon on the button" msgid "Show an icon on the button"
msgstr "Icon auf dem Button anzeigen" msgstr "Icon auf dem Button anzeigen"
#: includes/class-m365-login-admin.php:317 #: includes/class-m365-login-admin.php:362
msgid "Default: Microsoft logo" msgid "Default: Microsoft logo"
msgstr "Standard: Microsoft-Logo" msgstr "Standard: Microsoft-Logo"
#: includes/class-m365-login-admin.php:319 #: includes/class-m365-login-admin.php:364
msgid "Choose from media library" msgid "Choose from media library"
msgstr "Aus Mediathek wählen" msgstr "Aus Mediathek wählen"
#: includes/class-m365-login-admin.php:320 #: includes/class-m365-login-admin.php:365
msgid "Use Microsoft logo" msgid "Use Microsoft logo"
msgstr "Microsoft-Logo verwenden" msgstr "Microsoft-Logo verwenden"
#: includes/class-m365-login-admin.php:322 #: includes/class-m365-login-admin.php:367
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best." 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." msgstr "PNG, SVG, JPG oder WebP. Quadratische Bilder (z. B. 64×64 px) eignen sich am besten."
#: includes/class-m365-login-admin.php:330 #: includes/class-m365-login-admin.php:375
msgid "Background" msgid "Background"
msgstr "Hintergrund" msgstr "Hintergrund"
#: includes/class-m365-login-admin.php:331 #: includes/class-m365-login-admin.php:376
msgid "Background (hover)" msgid "Background (hover)"
msgstr "Hintergrund (Hover)" msgstr "Hintergrund (Hover)"
#: includes/class-m365-login-admin.php:332 #: includes/class-m365-login-admin.php:377
msgid "Text colour" msgid "Text colour"
msgstr "Textfarbe" msgstr "Textfarbe"
#: includes/class-m365-login-admin.php:333 #: includes/class-m365-login-admin.php:378
msgid "Border" msgid "Border"
msgstr "Rahmen" msgstr "Rahmen"
#: includes/class-m365-login-admin.php:346 #: includes/class-m365-login-admin.php:391
msgid "Corner radius" msgid "Corner radius"
msgstr "Eckenradius" msgstr "Eckenradius"
#: includes/class-m365-login-admin.php:350 #: includes/class-m365-login-admin.php:395
msgid "Position on the login page" msgid "Position on the login page"
msgstr "Position auf der Login-Seite" msgstr "Position auf der Login-Seite"
#: includes/class-m365-login-admin.php:352 #: includes/class-m365-login-admin.php:397
msgid "Below the login form" msgid "Below the login form"
msgstr "Unter dem Login-Formular" msgstr "Unter dem Login-Formular"
#: includes/class-m365-login-admin.php:353 #: includes/class-m365-login-admin.php:398
msgid "Above the login form" msgid "Above the login form"
msgstr "Über dem Login-Formular" msgstr "Über dem Login-Formular"
#: includes/class-m365-login-admin.php:359 #: includes/class-m365-login-admin.php:404
msgid "Quick presets" msgid "Quick presets"
msgstr "Schnellauswahl" msgstr "Schnellauswahl"
#: includes/class-m365-login-admin.php:360 #: includes/class-m365-login-admin.php:405
msgid "Microsoft dark" msgid "Microsoft dark"
msgstr "Microsoft dunkel" msgstr "Microsoft dunkel"
#: includes/class-m365-login-admin.php:361 #: includes/class-m365-login-admin.php:406
msgid "Microsoft light" msgid "Microsoft light"
msgstr "Microsoft hell" msgstr "Microsoft hell"
#: includes/class-m365-login-admin.php:362 #: includes/class-m365-login-admin.php:407
msgid "Azure blue" msgid "Azure blue"
msgstr "Azure-Blau" msgstr "Azure-Blau"
#: includes/class-m365-login-admin.php:363 #: includes/class-m365-login-admin.php:408
msgid "WordPress blue" msgid "WordPress blue"
msgstr "WordPress-Blau" msgstr "WordPress-Blau"
#: includes/class-m365-login-admin.php:371 #: includes/class-m365-login-admin.php:416
msgid "User matching & hardening" msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung" msgstr "Benutzerzuordnung & Härtung"
#: includes/class-m365-login-admin.php:372 #: includes/class-m365-login-admin.php:417
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 "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." 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:377 #: includes/class-m365-login-admin.php:422
msgid "Bind WordPress accounts to the Microsoft object ID" msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden" msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
#: includes/class-m365-login-admin.php:378 #: includes/class-m365-login-admin.php:423
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." 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." 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:385 #: includes/class-m365-login-admin.php:430
msgid "Fall back to the user principal name (UPN)" msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen" msgstr "Auf den User Principal Name (UPN) zurückgreifen"
#: includes/class-m365-login-admin.php:386 #: includes/class-m365-login-admin.php:431
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." 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." 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:393 #: includes/class-m365-login-admin.php:438
msgid "Keep users signed in (\"Remember me\")" msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)" msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
#: includes/class-m365-login-admin.php:394 #: includes/class-m365-login-admin.php:439
msgid "Issues a 14-day WordPress session instead of a browser session." msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung." msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
#: includes/class-m365-login-admin.php:399 #: includes/class-m365-login-admin.php:444
msgid "Allowed e-mail domains (optional)" msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)" msgstr "Erlaubte E-Mail-Domains (optional)"
#: includes/class-m365-login-admin.php:401 #: includes/class-m365-login-admin.php:446
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant." 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." 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:406 #: includes/class-m365-login-admin.php:451
msgid "Allowed Entra groups (optional)"
msgstr "Erlaubte Entra-Gruppen (optional)"
#: includes/class-m365-login-admin.php:452
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:455
msgid "Search groups"
msgstr "Gruppen suchen"
#: includes/class-m365-login-admin.php:457
msgid "Type a group name or paste an object ID…"
msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
#: includes/class-m365-login-admin.php:458
msgid "Search"
msgstr "Suchen"
#: includes/class-m365-login-admin.php:463
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:469
msgid "Selected groups"
msgstr "Ausgewählte Gruppen"
#: includes/class-m365-login-admin.php:470
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:480
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:485
msgid "Button-only mode"
msgstr "Nur-Button-Modus"
#: includes/class-m365-login-admin.php:486
msgid "Hide the username/password form and the \"Lost your password?\" link, and refuse password sign-ins on the login page. Application passwords, REST and XML-RPC are not affected."
msgstr "Blendet das Benutzername/Passwort-Formular und den Link „Passwort vergessen?“ aus und lehnt Passwort-Anmeldungen auf der Login-Seite ab. Anwendungspasswörter, REST und XML-RPC sind nicht betroffen."
#: includes/class-m365-login-admin.php:491
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:492
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:497
msgid "Fallback link (keep it secret)"
msgstr "Fallback-Link (geheim halten)"
#: includes/class-m365-login-admin.php:498
msgid "Opening this link shows the password form again in that browser for 30 minutes. 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. Sicher aufbewahren er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
#: includes/class-m365-login-admin.php:506
msgid "Generate a new key when saving"
msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
#: includes/class-m365-login-admin.php:509
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:515
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:524
msgid "What the plugin does to keep sign-ins safe" msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung" msgstr "So schützt das Plugin die Anmeldung"
#: includes/class-m365-login-admin.php:408 #: includes/class-m365-login-admin.php:526
msgid "OpenID Connect authorization code flow with PKCE (S256) no tokens ever pass through the browser." 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." msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) Tokens laufen nie durch den Browser."
#: includes/class-m365-login-admin.php:409 #: includes/class-m365-login-admin.php:527
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)." 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)." msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
#: includes/class-m365-login-admin.php:410 #: includes/class-m365-login-admin.php:528
msgid "ID token signature verified against Microsofts published signing keys; issuer, audience, tenant, expiry and nonce are checked." msgid "ID token signature verified against Microsofts 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." 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:411 #: includes/class-m365-login-admin.php:529
msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed." 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." msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert."
#: includes/class-m365-login-admin.php:417 #: includes/class-m365-login-admin.php:535
msgid "Save changes" msgid "Save changes"
msgstr "Änderungen speichern" msgstr "Änderungen speichern"
#: includes/class-m365-login-admin.php:423 #: includes/class-m365-login-admin.php:541
msgid "Redirect URI" msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)" msgstr "Umleitungs-URI (Redirect URI)"
#: includes/class-m365-login-admin.php:424 #: includes/class-m365-login-admin.php:542
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:" 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:" msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
#: includes/class-m365-login-admin.php:430 #: includes/class-m365-login-admin.php:548
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." 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." 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:433 #: includes/class-m365-login-admin.php:551
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS." 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." 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:438 #: includes/class-m365-login-admin.php:556
msgid "Setup in 5 steps" msgid "Setup in 5 steps"
msgstr "Einrichtung in 5 Schritten" msgstr "Einrichtung in 5 Schritten"
#: includes/class-m365-login-admin.php:440 #: includes/class-m365-login-admin.php:558
msgid "Open the Microsoft Entra admin center → App registrations → New registration." msgid "Open the Microsoft Entra admin center → App registrations → New registration."
msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung." msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung."
#: includes/class-m365-login-admin.php:441 #: includes/class-m365-login-admin.php:559
msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above." 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." 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:442 #: includes/class-m365-login-admin.php:560
msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page." 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." msgstr "Anwendungs-ID (Client) und Verzeichnis-ID (Mandant) von der Übersichtsseite kopieren."
#: includes/class-m365-login-admin.php:443 #: includes/class-m365-login-admin.php:561
msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)." 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." msgstr "Unter „Zertifikate & Geheimnisse“ einen geheimen Clientschlüssel erstellen und dessen Wert (nicht die ID) kopieren."
#: includes/class-m365-login-admin.php:444 #: includes/class-m365-login-admin.php:562
msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page." 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." msgstr "Unter „Tokenkonfiguration“ den optionalen Anspruch „email“ für ID-Tokens hinzufügen (empfohlen), dann diese Seite speichern."
#: includes/class-m365-login-admin.php:446 #: includes/class-m365-login-admin.php:564
msgid "Required API permission: openid, profile, email (delegated) granted by default." msgid "Required API permission: openid, profile, email (delegated) granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) standardmäßig vorhanden." msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) standardmäßig vorhanden."
#: includes/class-m365-login-admin.php:450 #: includes/class-m365-login-admin.php:565
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:569
msgid "Shortcode" msgid "Shortcode"
msgstr "Shortcode" msgstr "Shortcode"
#: includes/class-m365-login-admin.php:451 #: includes/class-m365-login-admin.php:570
msgid "Place the button on a custom login page:" msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:" msgstr "Button auf einer eigenen Login-Seite platzieren:"
#: includes/class-m365-login-auth.php:593 #: includes/class-m365-login-auth.php:136
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:751
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:758 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet." msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet." msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
#: includes/class-m365-login-auth.php:594 #: includes/class-m365-login-auth.php:759
msgid "The login request expired or was invalid. Please try again." msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen." msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
#: includes/class-m365-login-auth.php:595 #: includes/class-m365-login-auth.php:760
msgid "Microsoft sign-in was cancelled." msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen." msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
#: includes/class-m365-login-auth.php:596 #: includes/class-m365-login-auth.php:761
msgid "Microsoft returned an error. Please try again." msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen." msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
#: includes/class-m365-login-auth.php:597 #: includes/class-m365-login-auth.php:762
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator." 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." msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
#: includes/class-m365-login-auth.php:598 #: includes/class-m365-login-auth.php:763
msgid "The Microsoft sign-in could not be verified." msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden." msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
#: includes/class-m365-login-auth.php:599 #: includes/class-m365-login-auth.php:764
msgid "Your Microsoft account did not provide an e-mail address." msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt." msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
#: includes/class-m365-login-auth.php:600 #: includes/class-m365-login-auth.php:765
msgid "Your e-mail domain is not allowed to sign in here." msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen." msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
#: includes/class-m365-login-auth.php:601 #: includes/class-m365-login-auth.php:766
msgid "No WordPress account exists for your Microsoft e-mail address." 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." msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
#: includes/class-m365-login-auth.php:602 #: includes/class-m365-login-auth.php:767
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator." 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." msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
#: includes/class-m365-login-auth.php:603 #: includes/class-m365-login-auth.php:768
msgid "You are not allowed to sign in with this account." msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt." msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
#: includes/class-m365-login-settings.php:40 #: includes/class-m365-login-auth.php:769
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:770
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:771
msgid "The fallback key is not valid."
msgstr "Der Fallback-Schlüssel ist ungültig."
#: includes/class-m365-login-auth.php:772
msgid "Too many attempts. Please wait 15 minutes."
msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
#: includes/class-m365-login-graph.php:193
msgid "Group"
msgstr "Gruppe"
#: includes/class-m365-login-graph.php:195
msgid "Security group"
msgstr "Sicherheitsgruppe"
#: includes/class-m365-login-graph.php:197
msgid "Microsoft 365 group"
msgstr "Microsoft 365-Gruppe"
#: includes/class-m365-login-settings.php:44
msgid "Sign in with Microsoft" msgid "Sign in with Microsoft"
msgstr "Login mit Microsoft" msgstr "Login mit Microsoft"
#: includes/class-m365-login-settings.php:49 #: includes/class-m365-login-settings.php:53
msgid "or" msgid "or"
msgstr "oder" msgstr "oder"
#: includes/class-m365-login-settings.php:176 #: includes/class-m365-login-settings.php:245
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"." 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." msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
#: includes/class-m365-login-settings.php:184 #: includes/class-m365-login-settings.php:253
msgid "The application (client) ID must be a GUID." msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein." msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
#: includes/class-m365-login-settings.php:196 #: includes/class-m365-login-settings.php:265
msgid "The client secret contains invalid characters." msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen." msgstr "Das Client Secret enthält ungültige Zeichen."
#: includes/class-m365-login-settings.php:200 #: includes/class-m365-login-settings.php:269
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?" 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?" msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
#: includes/class-m365-login.php:94 #: includes/class-m365-login.php:102
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
#: includes/class-m365-login.php:105 #: includes/class-m365-login.php:113
msgid "M365 Login requires PHP 7.4 or newer." msgid "M365 Login requires PHP 7.4 or newer."
msgstr "M365 Login benötigt PHP 7.4 oder neuer." msgstr "M365 Login benötigt PHP 7.4 oder neuer."
#: includes/class-m365-login.php:106 includes/class-m365-login.php:115 #: includes/class-m365-login.php:114 includes/class-m365-login.php:123
msgid "Plugin activation failed" msgid "Plugin activation failed"
msgstr "Plugin-Aktivierung fehlgeschlagen" msgstr "Plugin-Aktivierung fehlgeschlagen"
#: includes/class-m365-login.php:114 #: includes/class-m365-login.php:122
msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)." msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)."
msgstr "M365 Login benötigt die PHP-Erweiterung OpenSSL (zur Prüfung der Microsoft-Token-Signaturen und zur Verschlüsselung des Client Secrets)." msgstr "M365 Login benötigt die PHP-Erweiterung OpenSSL (zur Prüfung der Microsoft-Token-Signaturen und zur Verschlüsselung des Client Secrets)."

Binary file not shown.

View file

@ -16,445 +16,587 @@ msgstr ""
"X-Generator: bin/make-pot.py\n" "X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n" "X-Domain: m365-login\n"
#: includes/class-m365-login-admin.php:63 includes/class-m365-login-admin.php:64 includes/class-m365-login-admin.php:203 #: includes/class-m365-login-admin.php:75 includes/class-m365-login-admin.php:76 includes/class-m365-login-admin.php:248
msgid "M365 Login" msgid "M365 Login"
msgstr "M365 Login" msgstr "M365 Login"
#: includes/class-m365-login-admin.php:102 #: includes/class-m365-login-admin.php:114
msgid "M365 Login is active but not connected to Microsoft Entra ID yet." 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." msgstr "M365 Login ist aktiv, aber noch nicht mit Microsoft Entra ID verbunden."
#: includes/class-m365-login-admin.php:104 #: includes/class-m365-login-admin.php:116
msgid "Open the settings" msgid "Open the settings"
msgstr "Einstellungen öffnen" msgstr "Einstellungen öffnen"
#: includes/class-m365-login-admin.php:132 #: includes/class-m365-login-admin.php:145
msgid "Choose button icon" msgid "Choose button icon"
msgstr "Button-Icon auswählen" msgstr "Button-Icon auswählen"
#: includes/class-m365-login-admin.php:133 #: includes/class-m365-login-admin.php:146
msgid "Use this icon" msgid "Use this icon"
msgstr "Dieses Icon verwenden" msgstr "Dieses Icon verwenden"
#: includes/class-m365-login-admin.php:134 #: includes/class-m365-login-admin.php:147
msgid "Copied!" msgid "Copied!"
msgstr "Kopiert!" msgstr "Kopiert!"
#: includes/class-m365-login-admin.php:135 includes/class-m365-login-admin.php:427 #: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:502 includes/class-m365-login-admin.php:545
msgid "Copy" msgid "Copy"
msgstr "Kopieren" msgstr "Kopieren"
#: includes/class-m365-login-admin.php:136 #: includes/class-m365-login-admin.php:149
msgid "Testing…" msgid "Testing…"
msgstr "Wird geprüft …" msgstr "Wird geprüft …"
#: includes/class-m365-login-admin.php:137 #: includes/class-m365-login-admin.php:150
msgid "The tenant could not be reached. Check the tenant ID and the servers outgoing connections." msgid "The tenant could not be reached. Check the tenant ID and the servers outgoing connections."
msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen." msgstr "Der Tenant ist nicht erreichbar. Bitte Tenant-ID und ausgehende Verbindungen des Servers prüfen."
#: includes/class-m365-login-admin.php:149 #: includes/class-m365-login-admin.php:151
msgid "No groups found."
msgstr "Keine Gruppen gefunden."
#: includes/class-m365-login-admin.php:152
msgid "Searching…"
msgstr "Suche läuft …"
#: includes/class-m365-login-admin.php:153
msgid "Add"
msgstr "Hinzufügen"
#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:476
msgid "Remove"
msgstr "Entfernen"
#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:461
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
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
msgid "You are not allowed to do this." msgid "You are not allowed to do this."
msgstr "Dafür fehlt die Berechtigung." msgstr "Dafür fehlt die Berechtigung."
#: includes/class-m365-login-admin.php:154 #: includes/class-m365-login-admin.php:173
msgid "Please enter a valid tenant ID first." msgid "Please enter a valid tenant ID first."
msgstr "Bitte zuerst eine gültige Tenant-ID eingeben." msgstr "Bitte zuerst eine gültige Tenant-ID eingeben."
#. translators: %d: HTTP status code #. translators: %d: HTTP status code
#: includes/class-m365-login-admin.php:168 #: includes/class-m365-login-admin.php:187
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?" msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?" msgstr "Microsoft hat mit HTTP %d geantwortet. Ist die Tenant-ID korrekt?"
#. translators: %d: HTTP status code #. translators: %d: HTTP status code
#: includes/class-m365-login-admin.php:177 #: includes/class-m365-login-admin.php:196
msgid "Tenant reachable. The OpenID configuration was loaded successfully." msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen." msgstr "Tenant erreichbar. Die OpenID-Konfiguration wurde erfolgreich geladen."
#: includes/class-m365-login-admin.php:187 #: includes/class-m365-login-admin.php:219
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
msgid "You are not allowed to access this page." msgid "You are not allowed to access this page."
msgstr "Für diese Seite fehlt die Berechtigung." msgstr "Für diese Seite fehlt die Berechtigung."
#: includes/class-m365-login-admin.php:204 #: includes/class-m365-login-admin.php:249
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account." 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." msgstr "Bestehende Benutzer melden sich mit ihrem Microsoft 365 / Entra ID-Konto an."
#: includes/class-m365-login-admin.php:209 #: includes/class-m365-login-admin.php:254
msgid "Connected" msgid "Connected"
msgstr "Verbunden" msgstr "Verbunden"
#: includes/class-m365-login-admin.php:209 #: includes/class-m365-login-admin.php:254
msgid "Setup incomplete" msgid "Setup incomplete"
msgstr "Einrichtung unvollständig" msgstr "Einrichtung unvollständig"
#: includes/class-m365-login-admin.php:217 #: includes/class-m365-login-admin.php:262
msgid "Connection" msgid "Connection"
msgstr "Verbindung" msgstr "Verbindung"
#: includes/class-m365-login-admin.php:218 #: includes/class-m365-login-admin.php:263
msgid "Button" msgid "Button"
msgstr "Button" msgstr "Button"
#: includes/class-m365-login-admin.php:219 #: includes/class-m365-login-admin.php:264
msgid "Security" msgid "Security"
msgstr "Sicherheit" msgstr "Sicherheit"
#: includes/class-m365-login-admin.php:228 #: includes/class-m365-login-admin.php:273
msgid "Microsoft Entra ID app registration" msgid "Microsoft Entra ID app registration"
msgstr "App-Registrierung in Microsoft Entra ID" msgstr "App-Registrierung in Microsoft Entra ID"
#: includes/class-m365-login-admin.php:229 #: includes/class-m365-login-admin.php:274
msgid "Enter the values from your app registration in the Microsoft Entra admin center." 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." msgstr "Tragen Sie hier die Werte aus Ihrer App-Registrierung im Microsoft Entra Admin Center ein."
#: includes/class-m365-login-admin.php:232 #: includes/class-m365-login-admin.php:277
msgid "Directory (tenant) ID" msgid "Directory (tenant) ID"
msgstr "Verzeichnis-ID (Mandant/Tenant)" msgstr "Verzeichnis-ID (Mandant/Tenant)"
#: includes/class-m365-login-admin.php:235 #: includes/class-m365-login-admin.php:280
msgid "Test tenant" msgid "Test tenant"
msgstr "Tenant testen" msgstr "Tenant testen"
#: includes/class-m365-login-admin.php:237 #: includes/class-m365-login-admin.php:282
msgid "Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. \"organizations\" allows any work or school account." 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." 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:242 #: includes/class-m365-login-admin.php:287
msgid "Application (client) ID" msgid "Application (client) ID"
msgstr "Anwendungs-ID (Client)" msgstr "Anwendungs-ID (Client)"
#: includes/class-m365-login-admin.php:247 #: includes/class-m365-login-admin.php:292
msgid "Client secret" msgid "Client secret"
msgstr "Geheimer Clientschlüssel (Client Secret)" msgstr "Geheimer Clientschlüssel (Client Secret)"
#: includes/class-m365-login-admin.php:249 #: includes/class-m365-login-admin.php:294
msgid "•••••••••••• (stored, leave empty to keep)" msgid "•••••••••••• (stored, leave empty to keep)"
msgstr "•••••••••••• (gespeichert leer lassen, um zu behalten)" msgstr "•••••••••••• (gespeichert leer lassen, um zu behalten)"
#: includes/class-m365-login-admin.php:249 #: includes/class-m365-login-admin.php:294
msgid "Paste the secret value" msgid "Paste the secret value"
msgstr "Wert des Secrets einfügen" msgstr "Wert des Secrets einfügen"
#: includes/class-m365-login-admin.php:250 #: includes/class-m365-login-admin.php:295
msgid "Show secret" msgid "Show secret"
msgstr "Secret anzeigen" msgstr "Secret anzeigen"
#: includes/class-m365-login-admin.php:255 #: includes/class-m365-login-admin.php:300
msgid "Remove the stored secret" msgid "Remove the stored secret"
msgstr "Gespeichertes Secret entfernen" msgstr "Gespeichertes Secret entfernen"
#: includes/class-m365-login-admin.php:258 #: 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." 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." 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:262 #: includes/class-m365-login-admin.php:307
msgid "Account prompt" msgid "Account prompt"
msgstr "Kontoauswahl" msgstr "Kontoauswahl"
#: includes/class-m365-login-admin.php:264 #: includes/class-m365-login-admin.php:309
msgid "Always let the user pick an account (recommended)" msgid "Always let the user pick an account (recommended)"
msgstr "Benutzer wählt immer ein Konto aus (empfohlen)" msgstr "Benutzer wählt immer ein Konto aus (empfohlen)"
#: includes/class-m365-login-admin.php:265 #: includes/class-m365-login-admin.php:310
msgid "Use the current Microsoft session if available" msgid "Use the current Microsoft session if available"
msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden" msgstr "Vorhandene Microsoft-Sitzung verwenden, falls vorhanden"
#: includes/class-m365-login-admin.php:266 #: includes/class-m365-login-admin.php:311
msgid "Always require re-entering credentials" msgid "Always require re-entering credentials"
msgstr "Immer erneute Eingabe der Anmeldedaten verlangen" msgstr "Immer erneute Eingabe der Anmeldedaten verlangen"
#: includes/class-m365-login-admin.php:275 #: includes/class-m365-login-admin.php:320
msgid "Appearance" msgid "Appearance"
msgstr "Darstellung" msgstr "Darstellung"
#: includes/class-m365-login-admin.php:278 #: includes/class-m365-login-admin.php:323
msgid "Live preview" msgid "Live preview"
msgstr "Live-Vorschau" msgstr "Live-Vorschau"
#: includes/class-m365-login-admin.php:292 #: includes/class-m365-login-admin.php:337
msgid "Button text" msgid "Button text"
msgstr "Button-Text" msgstr "Button-Text"
#: includes/class-m365-login-admin.php:296 #: includes/class-m365-login-admin.php:341
msgid "Divider text" msgid "Divider text"
msgstr "Trennlinien-Text" msgstr "Trennlinien-Text"
#: includes/class-m365-login-admin.php:298 #: includes/class-m365-login-admin.php:343
msgid "Leave empty to hide the divider line." msgid "Leave empty to hide the divider line."
msgstr "Leer lassen, um die Trennlinie auszublenden." msgstr "Leer lassen, um die Trennlinie auszublenden."
#: includes/class-m365-login-admin.php:303 #: includes/class-m365-login-admin.php:348
msgid "Icon" msgid "Icon"
msgstr "Icon" msgstr "Icon"
#: includes/class-m365-login-admin.php:306 #: includes/class-m365-login-admin.php:351
msgid "Show an icon on the button" msgid "Show an icon on the button"
msgstr "Icon auf dem Button anzeigen" msgstr "Icon auf dem Button anzeigen"
#: includes/class-m365-login-admin.php:317 #: includes/class-m365-login-admin.php:362
msgid "Default: Microsoft logo" msgid "Default: Microsoft logo"
msgstr "Standard: Microsoft-Logo" msgstr "Standard: Microsoft-Logo"
#: includes/class-m365-login-admin.php:319 #: includes/class-m365-login-admin.php:364
msgid "Choose from media library" msgid "Choose from media library"
msgstr "Aus Mediathek wählen" msgstr "Aus Mediathek wählen"
#: includes/class-m365-login-admin.php:320 #: includes/class-m365-login-admin.php:365
msgid "Use Microsoft logo" msgid "Use Microsoft logo"
msgstr "Microsoft-Logo verwenden" msgstr "Microsoft-Logo verwenden"
#: includes/class-m365-login-admin.php:322 #: includes/class-m365-login-admin.php:367
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best." 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." msgstr "PNG, SVG, JPG oder WebP. Quadratische Bilder (z. B. 64×64 px) eignen sich am besten."
#: includes/class-m365-login-admin.php:330 #: includes/class-m365-login-admin.php:375
msgid "Background" msgid "Background"
msgstr "Hintergrund" msgstr "Hintergrund"
#: includes/class-m365-login-admin.php:331 #: includes/class-m365-login-admin.php:376
msgid "Background (hover)" msgid "Background (hover)"
msgstr "Hintergrund (Hover)" msgstr "Hintergrund (Hover)"
#: includes/class-m365-login-admin.php:332 #: includes/class-m365-login-admin.php:377
msgid "Text colour" msgid "Text colour"
msgstr "Textfarbe" msgstr "Textfarbe"
#: includes/class-m365-login-admin.php:333 #: includes/class-m365-login-admin.php:378
msgid "Border" msgid "Border"
msgstr "Rahmen" msgstr "Rahmen"
#: includes/class-m365-login-admin.php:346 #: includes/class-m365-login-admin.php:391
msgid "Corner radius" msgid "Corner radius"
msgstr "Eckenradius" msgstr "Eckenradius"
#: includes/class-m365-login-admin.php:350 #: includes/class-m365-login-admin.php:395
msgid "Position on the login page" msgid "Position on the login page"
msgstr "Position auf der Login-Seite" msgstr "Position auf der Login-Seite"
#: includes/class-m365-login-admin.php:352 #: includes/class-m365-login-admin.php:397
msgid "Below the login form" msgid "Below the login form"
msgstr "Unter dem Login-Formular" msgstr "Unter dem Login-Formular"
#: includes/class-m365-login-admin.php:353 #: includes/class-m365-login-admin.php:398
msgid "Above the login form" msgid "Above the login form"
msgstr "Über dem Login-Formular" msgstr "Über dem Login-Formular"
#: includes/class-m365-login-admin.php:359 #: includes/class-m365-login-admin.php:404
msgid "Quick presets" msgid "Quick presets"
msgstr "Schnellauswahl" msgstr "Schnellauswahl"
#: includes/class-m365-login-admin.php:360 #: includes/class-m365-login-admin.php:405
msgid "Microsoft dark" msgid "Microsoft dark"
msgstr "Microsoft dunkel" msgstr "Microsoft dunkel"
#: includes/class-m365-login-admin.php:361 #: includes/class-m365-login-admin.php:406
msgid "Microsoft light" msgid "Microsoft light"
msgstr "Microsoft hell" msgstr "Microsoft hell"
#: includes/class-m365-login-admin.php:362 #: includes/class-m365-login-admin.php:407
msgid "Azure blue" msgid "Azure blue"
msgstr "Azure-Blau" msgstr "Azure-Blau"
#: includes/class-m365-login-admin.php:363 #: includes/class-m365-login-admin.php:408
msgid "WordPress blue" msgid "WordPress blue"
msgstr "WordPress-Blau" msgstr "WordPress-Blau"
#: includes/class-m365-login-admin.php:371 #: includes/class-m365-login-admin.php:416
msgid "User matching & hardening" msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung" msgstr "Benutzerzuordnung & Härtung"
#: includes/class-m365-login-admin.php:372 #: includes/class-m365-login-admin.php:417
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 "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." 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:377 #: includes/class-m365-login-admin.php:422
msgid "Bind WordPress accounts to the Microsoft object ID" msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden" msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
#: includes/class-m365-login-admin.php:378 #: includes/class-m365-login-admin.php:423
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." 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." 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:385 #: includes/class-m365-login-admin.php:430
msgid "Fall back to the user principal name (UPN)" msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen" msgstr "Auf den User Principal Name (UPN) zurückgreifen"
#: includes/class-m365-login-admin.php:386 #: includes/class-m365-login-admin.php:431
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." 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." 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:393 #: includes/class-m365-login-admin.php:438
msgid "Keep users signed in (\"Remember me\")" msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)" msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
#: includes/class-m365-login-admin.php:394 #: includes/class-m365-login-admin.php:439
msgid "Issues a 14-day WordPress session instead of a browser session." msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung." msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
#: includes/class-m365-login-admin.php:399 #: includes/class-m365-login-admin.php:444
msgid "Allowed e-mail domains (optional)" msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)" msgstr "Erlaubte E-Mail-Domains (optional)"
#: includes/class-m365-login-admin.php:401 #: includes/class-m365-login-admin.php:446
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant." 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." 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:406 #: includes/class-m365-login-admin.php:451
msgid "Allowed Entra groups (optional)"
msgstr "Erlaubte Entra-Gruppen (optional)"
#: includes/class-m365-login-admin.php:452
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:455
msgid "Search groups"
msgstr "Gruppen suchen"
#: includes/class-m365-login-admin.php:457
msgid "Type a group name or paste an object ID…"
msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
#: includes/class-m365-login-admin.php:458
msgid "Search"
msgstr "Suchen"
#: includes/class-m365-login-admin.php:463
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:469
msgid "Selected groups"
msgstr "Ausgewählte Gruppen"
#: includes/class-m365-login-admin.php:470
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:480
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:485
msgid "Button-only mode"
msgstr "Nur-Button-Modus"
#: includes/class-m365-login-admin.php:486
msgid "Hide the username/password form and the \"Lost your password?\" link, and refuse password sign-ins on the login page. Application passwords, REST and XML-RPC are not affected."
msgstr "Blendet das Benutzername/Passwort-Formular und den Link „Passwort vergessen?“ aus und lehnt Passwort-Anmeldungen auf der Login-Seite ab. Anwendungspasswörter, REST und XML-RPC sind nicht betroffen."
#: includes/class-m365-login-admin.php:491
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:492
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:497
msgid "Fallback link (keep it secret)"
msgstr "Fallback-Link (geheim halten)"
#: includes/class-m365-login-admin.php:498
msgid "Opening this link shows the password form again in that browser for 30 minutes. 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. Sicher aufbewahren er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
#: includes/class-m365-login-admin.php:506
msgid "Generate a new key when saving"
msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
#: includes/class-m365-login-admin.php:509
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:515
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:524
msgid "What the plugin does to keep sign-ins safe" msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung" msgstr "So schützt das Plugin die Anmeldung"
#: includes/class-m365-login-admin.php:408 #: includes/class-m365-login-admin.php:526
msgid "OpenID Connect authorization code flow with PKCE (S256) no tokens ever pass through the browser." 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." msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) Tokens laufen nie durch den Browser."
#: includes/class-m365-login-admin.php:409 #: includes/class-m365-login-admin.php:527
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)." 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)." msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
#: includes/class-m365-login-admin.php:410 #: includes/class-m365-login-admin.php:528
msgid "ID token signature verified against Microsofts published signing keys; issuer, audience, tenant, expiry and nonce are checked." msgid "ID token signature verified against Microsofts 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." 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:411 #: includes/class-m365-login-admin.php:529
msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed." 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." msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert."
#: includes/class-m365-login-admin.php:417 #: includes/class-m365-login-admin.php:535
msgid "Save changes" msgid "Save changes"
msgstr "Änderungen speichern" msgstr "Änderungen speichern"
#: includes/class-m365-login-admin.php:423 #: includes/class-m365-login-admin.php:541
msgid "Redirect URI" msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)" msgstr "Umleitungs-URI (Redirect URI)"
#: includes/class-m365-login-admin.php:424 #: includes/class-m365-login-admin.php:542
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:" 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:" msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
#: includes/class-m365-login-admin.php:430 #: includes/class-m365-login-admin.php:548
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." 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." 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:433 #: includes/class-m365-login-admin.php:551
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS." 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." 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:438 #: includes/class-m365-login-admin.php:556
msgid "Setup in 5 steps" msgid "Setup in 5 steps"
msgstr "Einrichtung in 5 Schritten" msgstr "Einrichtung in 5 Schritten"
#: includes/class-m365-login-admin.php:440 #: includes/class-m365-login-admin.php:558
msgid "Open the Microsoft Entra admin center → App registrations → New registration." msgid "Open the Microsoft Entra admin center → App registrations → New registration."
msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung." msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung."
#: includes/class-m365-login-admin.php:441 #: includes/class-m365-login-admin.php:559
msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above." 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." 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:442 #: includes/class-m365-login-admin.php:560
msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page." 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." msgstr "Anwendungs-ID (Client) und Verzeichnis-ID (Mandant) von der Übersichtsseite kopieren."
#: includes/class-m365-login-admin.php:443 #: includes/class-m365-login-admin.php:561
msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)." 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." msgstr "Unter „Zertifikate & Geheimnisse“ einen geheimen Clientschlüssel erstellen und dessen Wert (nicht die ID) kopieren."
#: includes/class-m365-login-admin.php:444 #: includes/class-m365-login-admin.php:562
msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page." 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." msgstr "Unter „Tokenkonfiguration“ den optionalen Anspruch „email“ für ID-Tokens hinzufügen (empfohlen), dann diese Seite speichern."
#: includes/class-m365-login-admin.php:446 #: includes/class-m365-login-admin.php:564
msgid "Required API permission: openid, profile, email (delegated) granted by default." msgid "Required API permission: openid, profile, email (delegated) granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) standardmäßig vorhanden." msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) standardmäßig vorhanden."
#: includes/class-m365-login-admin.php:450 #: includes/class-m365-login-admin.php:565
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:569
msgid "Shortcode" msgid "Shortcode"
msgstr "Shortcode" msgstr "Shortcode"
#: includes/class-m365-login-admin.php:451 #: includes/class-m365-login-admin.php:570
msgid "Place the button on a custom login page:" msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:" msgstr "Button auf einer eigenen Login-Seite platzieren:"
#: includes/class-m365-login-auth.php:593 #: includes/class-m365-login-auth.php:136
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:751
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:758 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet." msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet." msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
#: includes/class-m365-login-auth.php:594 #: includes/class-m365-login-auth.php:759
msgid "The login request expired or was invalid. Please try again." msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen." msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
#: includes/class-m365-login-auth.php:595 #: includes/class-m365-login-auth.php:760
msgid "Microsoft sign-in was cancelled." msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen." msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
#: includes/class-m365-login-auth.php:596 #: includes/class-m365-login-auth.php:761
msgid "Microsoft returned an error. Please try again." msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen." msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
#: includes/class-m365-login-auth.php:597 #: includes/class-m365-login-auth.php:762
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator." 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." msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
#: includes/class-m365-login-auth.php:598 #: includes/class-m365-login-auth.php:763
msgid "The Microsoft sign-in could not be verified." msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden." msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
#: includes/class-m365-login-auth.php:599 #: includes/class-m365-login-auth.php:764
msgid "Your Microsoft account did not provide an e-mail address." msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt." msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
#: includes/class-m365-login-auth.php:600 #: includes/class-m365-login-auth.php:765
msgid "Your e-mail domain is not allowed to sign in here." msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen." msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
#: includes/class-m365-login-auth.php:601 #: includes/class-m365-login-auth.php:766
msgid "No WordPress account exists for your Microsoft e-mail address." 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." msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
#: includes/class-m365-login-auth.php:602 #: includes/class-m365-login-auth.php:767
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator." 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." msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
#: includes/class-m365-login-auth.php:603 #: includes/class-m365-login-auth.php:768
msgid "You are not allowed to sign in with this account." msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt." msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
#: includes/class-m365-login-settings.php:40 #: includes/class-m365-login-auth.php:769
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:770
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:771
msgid "The fallback key is not valid."
msgstr "Der Fallback-Schlüssel ist ungültig."
#: includes/class-m365-login-auth.php:772
msgid "Too many attempts. Please wait 15 minutes."
msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
#: includes/class-m365-login-graph.php:193
msgid "Group"
msgstr "Gruppe"
#: includes/class-m365-login-graph.php:195
msgid "Security group"
msgstr "Sicherheitsgruppe"
#: includes/class-m365-login-graph.php:197
msgid "Microsoft 365 group"
msgstr "Microsoft 365-Gruppe"
#: includes/class-m365-login-settings.php:44
msgid "Sign in with Microsoft" msgid "Sign in with Microsoft"
msgstr "Login mit Microsoft" msgstr "Login mit Microsoft"
#: includes/class-m365-login-settings.php:49 #: includes/class-m365-login-settings.php:53
msgid "or" msgid "or"
msgstr "oder" msgstr "oder"
#: includes/class-m365-login-settings.php:176 #: includes/class-m365-login-settings.php:245
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"." 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." msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
#: includes/class-m365-login-settings.php:184 #: includes/class-m365-login-settings.php:253
msgid "The application (client) ID must be a GUID." msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein." msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
#: includes/class-m365-login-settings.php:196 #: includes/class-m365-login-settings.php:265
msgid "The client secret contains invalid characters." msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen." msgstr "Das Client Secret enthält ungültige Zeichen."
#: includes/class-m365-login-settings.php:200 #: includes/class-m365-login-settings.php:269
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?" 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?" msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
#: includes/class-m365-login.php:94 #: includes/class-m365-login.php:102
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
#: includes/class-m365-login.php:105 #: includes/class-m365-login.php:113
msgid "M365 Login requires PHP 7.4 or newer." msgid "M365 Login requires PHP 7.4 or newer."
msgstr "M365 Login benötigt PHP 7.4 oder neuer." msgstr "M365 Login benötigt PHP 7.4 oder neuer."
#: includes/class-m365-login.php:106 includes/class-m365-login.php:115 #: includes/class-m365-login.php:114 includes/class-m365-login.php:123
msgid "Plugin activation failed" msgid "Plugin activation failed"
msgstr "Plugin-Aktivierung fehlgeschlagen" msgstr "Plugin-Aktivierung fehlgeschlagen"
#: includes/class-m365-login.php:114 #: includes/class-m365-login.php:122
msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)." msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)."
msgstr "M365 Login benötigt die PHP-Erweiterung OpenSSL (zur Prüfung der Microsoft-Token-Signaturen und zur Verschlüsselung des Client Secrets)." msgstr "M365 Login benötigt die PHP-Erweiterung OpenSSL (zur Prüfung der Microsoft-Token-Signaturen und zur Verschlüsselung des Client Secrets)."

View file

@ -14,445 +14,587 @@ msgstr ""
"X-Generator: bin/make-pot.py\n" "X-Generator: bin/make-pot.py\n"
"X-Domain: m365-login\n" "X-Domain: m365-login\n"
#: includes/class-m365-login-admin.php:63 includes/class-m365-login-admin.php:64 includes/class-m365-login-admin.php:203 #: includes/class-m365-login-admin.php:75 includes/class-m365-login-admin.php:76 includes/class-m365-login-admin.php:248
msgid "M365 Login" msgid "M365 Login"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:102 #: includes/class-m365-login-admin.php:114
msgid "M365 Login is active but not connected to Microsoft Entra ID yet." msgid "M365 Login is active but not connected to Microsoft Entra ID yet."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:104 #: includes/class-m365-login-admin.php:116
msgid "Open the settings" msgid "Open the settings"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:132 #: includes/class-m365-login-admin.php:145
msgid "Choose button icon" msgid "Choose button icon"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:133 #: includes/class-m365-login-admin.php:146
msgid "Use this icon" msgid "Use this icon"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:134 #: includes/class-m365-login-admin.php:147
msgid "Copied!" msgid "Copied!"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:135 includes/class-m365-login-admin.php:427 #: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:502 includes/class-m365-login-admin.php:545
msgid "Copy" msgid "Copy"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:136 #: includes/class-m365-login-admin.php:149
msgid "Testing…" msgid "Testing…"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:137 #: includes/class-m365-login-admin.php:150
msgid "The tenant could not be reached. Check the tenant ID and the servers outgoing connections." msgid "The tenant could not be reached. Check the tenant ID and the servers outgoing connections."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:149 #: includes/class-m365-login-admin.php:151
msgid "No groups found."
msgstr ""
#: includes/class-m365-login-admin.php:152
msgid "Searching…"
msgstr ""
#: includes/class-m365-login-admin.php:153
msgid "Add"
msgstr ""
#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:476
msgid "Remove"
msgstr ""
#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:461
msgid "Save the connection settings first, then search for groups."
msgstr ""
#: includes/class-m365-login-admin.php:156
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
msgid "You are not allowed to do this." msgid "You are not allowed to do this."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:154 #: includes/class-m365-login-admin.php:173
msgid "Please enter a valid tenant ID first." msgid "Please enter a valid tenant ID first."
msgstr "" msgstr ""
#. translators: %d: HTTP status code #. translators: %d: HTTP status code
#: includes/class-m365-login-admin.php:168 #: includes/class-m365-login-admin.php:187
msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?" msgid "Microsoft answered with HTTP %d. Is the tenant ID correct?"
msgstr "" msgstr ""
#. translators: %d: HTTP status code #. translators: %d: HTTP status code
#: includes/class-m365-login-admin.php:177 #: includes/class-m365-login-admin.php:196
msgid "Tenant reachable. The OpenID configuration was loaded successfully." msgid "Tenant reachable. The OpenID configuration was loaded successfully."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:187
msgid "You are not allowed to access this page."
msgstr ""
#: includes/class-m365-login-admin.php:204
msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr ""
#: includes/class-m365-login-admin.php:209
msgid "Connected"
msgstr ""
#: includes/class-m365-login-admin.php:209
msgid "Setup incomplete"
msgstr ""
#: includes/class-m365-login-admin.php:217
msgid "Connection"
msgstr ""
#: includes/class-m365-login-admin.php:218
msgid "Button"
msgstr ""
#: includes/class-m365-login-admin.php:219 #: includes/class-m365-login-admin.php:219
msgid "Security" 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:228
msgid "Microsoft Entra ID app registration"
msgstr ""
#: includes/class-m365-login-admin.php:229
msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:232 #: includes/class-m365-login-admin.php:232
msgid "Directory (tenant) ID" msgid "You are not allowed to access this page."
msgstr ""
#: includes/class-m365-login-admin.php:235
msgid "Test tenant"
msgstr ""
#: includes/class-m365-login-admin.php:237
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:242
msgid "Application (client) ID"
msgstr ""
#: includes/class-m365-login-admin.php:247
msgid "Client secret"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:249 #: includes/class-m365-login-admin.php:249
msgid "•••••••••••• (stored, leave empty to keep)" msgid "Let existing users sign in with their Microsoft 365 / Entra ID account."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:249 #: includes/class-m365-login-admin.php:254
msgid "Paste the secret value" msgid "Connected"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:250 #: includes/class-m365-login-admin.php:254
msgid "Show secret" msgid "Setup incomplete"
msgstr ""
#: includes/class-m365-login-admin.php:255
msgid "Remove the stored secret"
msgstr ""
#: includes/class-m365-login-admin.php:258
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 "" msgstr ""
#: includes/class-m365-login-admin.php:262 #: includes/class-m365-login-admin.php:262
msgid "Account prompt" msgid "Connection"
msgstr ""
#: includes/class-m365-login-admin.php:263
msgid "Button"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:264 #: includes/class-m365-login-admin.php:264
msgid "Always let the user pick an account (recommended)" msgid "Security"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:265 #: includes/class-m365-login-admin.php:273
msgid "Use the current Microsoft session if available" msgid "Microsoft Entra ID app registration"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:266 #: includes/class-m365-login-admin.php:274
msgid "Always require re-entering credentials" msgid "Enter the values from your app registration in the Microsoft Entra admin center."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:275 #: includes/class-m365-login-admin.php:277
msgid "Appearance" msgid "Directory (tenant) ID"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:278 #: includes/class-m365-login-admin.php:280
msgid "Live preview" msgid "Test tenant"
msgstr ""
#: includes/class-m365-login-admin.php:282
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
msgid "Application (client) ID"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:292 #: includes/class-m365-login-admin.php:292
msgid "Button text" msgid "Client secret"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:296 #: includes/class-m365-login-admin.php:294
msgid "Divider text" msgid "•••••••••••• (stored, leave empty to keep)"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:298 #: includes/class-m365-login-admin.php:294
msgid "Leave empty to hide the divider line." msgid "Paste the secret value"
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 "" msgstr ""
#: includes/class-m365-login-admin.php:303 #: includes/class-m365-login-admin.php:303
msgid "Icon" 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 "" msgstr ""
#: includes/class-m365-login-admin.php:306 #: includes/class-m365-login-admin.php:307
msgid "Show an icon on the button" msgid "Account prompt"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:317 #: includes/class-m365-login-admin.php:309
msgid "Default: Microsoft logo" msgid "Always let the user pick an account (recommended)"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:319 #: includes/class-m365-login-admin.php:310
msgid "Choose from media library" msgid "Use the current Microsoft session if available"
msgstr ""
#: includes/class-m365-login-admin.php:311
msgid "Always require re-entering credentials"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:320 #: includes/class-m365-login-admin.php:320
msgid "Use Microsoft logo" msgid "Appearance"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:322 #: includes/class-m365-login-admin.php:323
msgid "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best." msgid "Live preview"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:330 #: includes/class-m365-login-admin.php:337
msgid "Background" msgid "Button text"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:331 #: includes/class-m365-login-admin.php:341
msgid "Background (hover)" msgid "Divider text"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:332 #: includes/class-m365-login-admin.php:343
msgid "Text colour" msgid "Leave empty to hide the divider line."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:333 #: includes/class-m365-login-admin.php:348
msgid "Border" msgid "Icon"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:346 #: includes/class-m365-login-admin.php:351
msgid "Corner radius" msgid "Show an icon on the button"
msgstr ""
#: includes/class-m365-login-admin.php:350
msgid "Position on the login page"
msgstr ""
#: includes/class-m365-login-admin.php:352
msgid "Below the login form"
msgstr ""
#: includes/class-m365-login-admin.php:353
msgid "Above the login form"
msgstr ""
#: includes/class-m365-login-admin.php:359
msgid "Quick presets"
msgstr ""
#: includes/class-m365-login-admin.php:360
msgid "Microsoft dark"
msgstr ""
#: includes/class-m365-login-admin.php:361
msgid "Microsoft light"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:362 #: includes/class-m365-login-admin.php:362
msgid "Azure blue" msgid "Default: Microsoft logo"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:363 #: includes/class-m365-login-admin.php:364
msgid "WordPress blue" msgid "Choose from media library"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:371 #: includes/class-m365-login-admin.php:365
msgid "User matching & hardening" msgid "Use Microsoft logo"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:372 #: includes/class-m365-login-admin.php:367
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 "PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best."
msgstr ""
#: includes/class-m365-login-admin.php:375
msgid "Background"
msgstr ""
#: includes/class-m365-login-admin.php:376
msgid "Background (hover)"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:377 #: includes/class-m365-login-admin.php:377
msgid "Bind WordPress accounts to the Microsoft object ID" msgid "Text colour"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:378 #: includes/class-m365-login-admin.php:378
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." msgid "Border"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:385 #: includes/class-m365-login-admin.php:391
msgid "Fall back to the user principal name (UPN)" msgid "Corner radius"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:386 #: includes/class-m365-login-admin.php:395
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." msgid "Position on the login page"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:393 #: includes/class-m365-login-admin.php:397
msgid "Keep users signed in (\"Remember me\")" msgid "Below the login form"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:394 #: includes/class-m365-login-admin.php:398
msgid "Issues a 14-day WordPress session instead of a browser session." msgid "Above the login form"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:399 #: includes/class-m365-login-admin.php:404
msgid "Allowed e-mail domains (optional)" msgid "Quick presets"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:401 #: includes/class-m365-login-admin.php:405
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant." msgid "Microsoft dark"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:406 #: includes/class-m365-login-admin.php:406
msgid "What the plugin does to keep sign-ins safe" msgid "Microsoft light"
msgstr ""
#: includes/class-m365-login-admin.php:407
msgid "Azure blue"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:408 #: includes/class-m365-login-admin.php:408
msgid "OpenID Connect authorization code flow with PKCE (S256) no tokens ever pass through the browser." msgid "WordPress blue"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:409 #: includes/class-m365-login-admin.php:416
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)." msgid "User matching & hardening"
msgstr ""
#: includes/class-m365-login-admin.php:410
msgid "ID token signature verified against Microsofts published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr ""
#: includes/class-m365-login-admin.php:411
msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:417 #: includes/class-m365-login-admin.php:417
msgid "Save changes" 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:422
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:423 #: includes/class-m365-login-admin.php:423
msgid "Redirect URI" 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:424
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:430 #: includes/class-m365-login-admin.php:430
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." msgid "Fall back to the user principal name (UPN)"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:433 #: includes/class-m365-login-admin.php:431
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS." 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 "" msgstr ""
#: includes/class-m365-login-admin.php:438 #: includes/class-m365-login-admin.php:438
msgid "Setup in 5 steps" msgid "Keep users signed in (\"Remember me\")"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:440 #: includes/class-m365-login-admin.php:439
msgid "Open the Microsoft Entra admin center → App registrations → New registration." msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr ""
#: includes/class-m365-login-admin.php:441
msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above."
msgstr ""
#: includes/class-m365-login-admin.php:442
msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page."
msgstr ""
#: includes/class-m365-login-admin.php:443
msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)."
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:444 #: includes/class-m365-login-admin.php:444
msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page." msgid "Allowed e-mail domains (optional)"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:446 #: includes/class-m365-login-admin.php:446
msgid "Required API permission: openid, profile, email (delegated) granted by default." 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:450
msgid "Shortcode"
msgstr "" msgstr ""
#: includes/class-m365-login-admin.php:451 #: includes/class-m365-login-admin.php:451
msgid "Allowed Entra groups (optional)"
msgstr ""
#: includes/class-m365-login-admin.php:452
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:455
msgid "Search groups"
msgstr ""
#: includes/class-m365-login-admin.php:457
msgid "Type a group name or paste an object ID…"
msgstr ""
#: includes/class-m365-login-admin.php:458
msgid "Search"
msgstr ""
#: includes/class-m365-login-admin.php:463
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:469
msgid "Selected groups"
msgstr ""
#: includes/class-m365-login-admin.php:470
msgid "No groups selected every matched user may sign in."
msgstr ""
#: includes/class-m365-login-admin.php:480
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:485
msgid "Button-only mode"
msgstr ""
#: includes/class-m365-login-admin.php:486
msgid "Hide the username/password form and the \"Lost your password?\" link, and refuse password sign-ins on the login page. Application passwords, REST and XML-RPC are not affected."
msgstr ""
#: includes/class-m365-login-admin.php:491
msgid "Show only the Microsoft button on the login page"
msgstr ""
#: includes/class-m365-login-admin.php:492
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:497
msgid "Fallback link (keep it secret)"
msgstr ""
#: includes/class-m365-login-admin.php:498
msgid "Opening this link shows the password form again in that browser for 30 minutes. Bookmark it somewhere safe it is your way back in if Microsoft sign-in ever breaks."
msgstr ""
#: includes/class-m365-login-admin.php:506
msgid "Generate a new key when saving"
msgstr ""
#: includes/class-m365-login-admin.php:509
msgid "A key is generated automatically the first time you save these settings."
msgstr ""
#. translators: %s: PHP constant
#: includes/class-m365-login-admin.php:515
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:524
msgid "What the plugin does to keep sign-ins safe"
msgstr ""
#: includes/class-m365-login-admin.php:526
msgid "OpenID Connect authorization code flow with PKCE (S256) no tokens ever pass through the browser."
msgstr ""
#: includes/class-m365-login-admin.php:527
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:528
msgid "ID token signature verified against Microsofts published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr ""
#: includes/class-m365-login-admin.php:529
msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
msgstr ""
#: includes/class-m365-login-admin.php:535
msgid "Save changes"
msgstr ""
#: includes/class-m365-login-admin.php:541
msgid "Redirect URI"
msgstr ""
#: includes/class-m365-login-admin.php:542
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr ""
#: includes/class-m365-login-admin.php:548
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:551
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:556
msgid "Setup in 5 steps"
msgstr ""
#: includes/class-m365-login-admin.php:558
msgid "Open the Microsoft Entra admin center → App registrations → New registration."
msgstr ""
#: includes/class-m365-login-admin.php:559
msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above."
msgstr ""
#: includes/class-m365-login-admin.php:560
msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page."
msgstr ""
#: includes/class-m365-login-admin.php:561
msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)."
msgstr ""
#: includes/class-m365-login-admin.php:562
msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page."
msgstr ""
#: includes/class-m365-login-admin.php:564
msgid "Required API permission: openid, profile, email (delegated) granted by default."
msgstr ""
#: includes/class-m365-login-admin.php:565
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:569
msgid "Shortcode"
msgstr ""
#: includes/class-m365-login-admin.php:570
msgid "Place the button on a custom login page:" msgid "Place the button on a custom login page:"
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:593 #: includes/class-m365-login-auth.php:136
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr ""
#: includes/class-m365-login-auth.php:751
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr ""
#: includes/class-m365-login-auth.php:758 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet." msgid "Microsoft login is not configured yet."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:594 #: includes/class-m365-login-auth.php:759
msgid "The login request expired or was invalid. Please try again." msgid "The login request expired or was invalid. Please try again."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:595 #: includes/class-m365-login-auth.php:760
msgid "Microsoft sign-in was cancelled." msgid "Microsoft sign-in was cancelled."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:596 #: includes/class-m365-login-auth.php:761
msgid "Microsoft returned an error. Please try again." msgid "Microsoft returned an error. Please try again."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:597 #: includes/class-m365-login-auth.php:762
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator." msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:598 #: includes/class-m365-login-auth.php:763
msgid "The Microsoft sign-in could not be verified." msgid "The Microsoft sign-in could not be verified."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:599 #: includes/class-m365-login-auth.php:764
msgid "Your Microsoft account did not provide an e-mail address." msgid "Your Microsoft account did not provide an e-mail address."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:600 #: includes/class-m365-login-auth.php:765
msgid "Your e-mail domain is not allowed to sign in here." msgid "Your e-mail domain is not allowed to sign in here."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:601 #: includes/class-m365-login-auth.php:766
msgid "No WordPress account exists for your Microsoft e-mail address." msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:602 #: includes/class-m365-login-auth.php:767
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator." msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr "" msgstr ""
#: includes/class-m365-login-auth.php:603 #: includes/class-m365-login-auth.php:768
msgid "You are not allowed to sign in with this account." msgid "You are not allowed to sign in with this account."
msgstr "" msgstr ""
#: includes/class-m365-login-settings.php:40 #: includes/class-m365-login-auth.php:769
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:770
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr ""
#: includes/class-m365-login-auth.php:771
msgid "The fallback key is not valid."
msgstr ""
#: includes/class-m365-login-auth.php:772
msgid "Too many attempts. Please wait 15 minutes."
msgstr ""
#: includes/class-m365-login-graph.php:193
msgid "Group"
msgstr ""
#: includes/class-m365-login-graph.php:195
msgid "Security group"
msgstr ""
#: includes/class-m365-login-graph.php:197
msgid "Microsoft 365 group"
msgstr ""
#: includes/class-m365-login-settings.php:44
msgid "Sign in with Microsoft" msgid "Sign in with Microsoft"
msgstr "" msgstr ""
#: includes/class-m365-login-settings.php:49 #: includes/class-m365-login-settings.php:53
msgid "or" msgid "or"
msgstr "" msgstr ""
#: includes/class-m365-login-settings.php:176 #: includes/class-m365-login-settings.php:245
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"." msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr "" msgstr ""
#: includes/class-m365-login-settings.php:184 #: includes/class-m365-login-settings.php:253
msgid "The application (client) ID must be a GUID." msgid "The application (client) ID must be a GUID."
msgstr "" msgstr ""
#: includes/class-m365-login-settings.php:196 #: includes/class-m365-login-settings.php:265
msgid "The client secret contains invalid characters." msgid "The client secret contains invalid characters."
msgstr "" msgstr ""
#: includes/class-m365-login-settings.php:200 #: includes/class-m365-login-settings.php:269
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?" msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr "" msgstr ""
#: includes/class-m365-login.php:94 #: includes/class-m365-login.php:102
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
#: includes/class-m365-login.php:105 #: includes/class-m365-login.php:113
msgid "M365 Login requires PHP 7.4 or newer." msgid "M365 Login requires PHP 7.4 or newer."
msgstr "" msgstr ""
#: includes/class-m365-login.php:106 includes/class-m365-login.php:115 #: includes/class-m365-login.php:114 includes/class-m365-login.php:123
msgid "Plugin activation failed" msgid "Plugin activation failed"
msgstr "" msgstr ""
#: includes/class-m365-login.php:114 #: includes/class-m365-login.php:122
msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)." msgid "M365 Login requires the PHP OpenSSL extension (needed to verify Microsoft token signatures and to encrypt the client secret)."
msgstr "" msgstr ""

View file

@ -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-settings.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-crypto.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-jwt.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-auth.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-button.php'; require_once M365_LOGIN_DIR . 'includes/class-m365-login-button.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-admin.php'; require_once M365_LOGIN_DIR . 'includes/class-m365-login-admin.php';

View file

@ -19,6 +19,8 @@ The plugin is deliberately small and strict:
* **No user provisioning.** A Microsoft sign-in succeeds only when a WordPress user with the same e-mail address already exists. Nobody gets an account just by having a Microsoft login. * **No user provisioning.** A Microsoft sign-in succeeds only when a WordPress user with the same e-mail address already exists. Nobody gets an account just by having a Microsoft login.
* **Password login stays available.** The button is an additional option; the normal form is untouched. * **Password login stays available.** The button is an additional option; the normal form is untouched.
* **Fully customisable button.** Change the text, replace the Microsoft logo with your own icon from the media library, pick background, hover, text and border colours, adjust the corner radius, and choose whether the button appears above or below the login form with a live preview. * **Fully customisable button.** Change the text, replace the Microsoft logo with your own icon from the media library, pick background, hover, text and border colours, adjust the corner radius, and choose whether the button appears above or below the login form with a live preview.
* **Entra group restriction.** Search and pick the groups whose members may sign in, right in the settings screen. Membership is checked via the ID token's `groups` claim or Microsoft Graph (nested groups included).
* **Button-only mode.** Hide the username/password form and refuse password sign-ins on the login page. A secret fallback link (and a `wp-config.php` constant) brings the form back when you need it.
* **Clean settings screen** with a copy-and-paste redirect URI, a tenant connectivity test and a five-step setup guide. * **Clean settings screen** with a copy-and-paste redirect URI, a tenant connectivity test and a five-step setup guide.
* **Shortcode** `[m365_login_button]` for custom login pages. * **Shortcode** `[m365_login_button]` for custom login pages.
@ -29,7 +31,8 @@ The plugin is deliberately small and strict:
* The **ID token signature is verified** against Microsoft's published signing keys (JWKS, cached and refreshed on key rollover). Issuer, audience, tenant, expiry, not-before and nonce are all checked. Only RS256 is accepted. * The **ID token signature is verified** against Microsoft's published signing keys (JWKS, cached and refreshed on key rollover). Issuer, audience, tenant, expiry, not-before and nonce are all checked. Only RS256 is accepted.
* Optional **tenant pinning**: when a tenant GUID is configured, tokens from any other tenant are rejected. * Optional **tenant pinning**: when a tenant GUID is configured, tokens from any other tenant are rejected.
* **Account binding**: on first sign-in the immutable Microsoft object ID is stored with the user; later sign-ins with the same e-mail but a different Microsoft identity are refused. * **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**. * 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. * The **client secret is encrypted at rest** (AES-256-GCM, key derived from your WordPress salts) and never displayed again.
* Every setting is sanitised, every output escaped, every admin request nonce- and capability-checked. * Every setting is sanitised, every output escaped, every admin request nonce- and capability-checked.
@ -52,6 +55,11 @@ Endpoints used (all under `https://login.microsoftonline.com/`):
* `/{tenant}/discovery/v2.0/keys` the server downloads Microsoft's public signing keys to verify the ID token. No user data is sent. * `/{tenant}/discovery/v2.0/keys` the server downloads Microsoft's public signing keys to verify the ID token. No user data is sent.
* `/{tenant}/v2.0/.well-known/openid-configuration` fetched only when an administrator clicks "Test tenant". No user data is sent. * `/{tenant}/v2.0/.well-known/openid-configuration` fetched only when an administrator clicks "Test tenant". No user data is sent.
When the optional **group restriction** is configured, the plugin additionally connects to **Microsoft Graph** (`https://graph.microsoft.com/v1.0/`) using an application token obtained from `/{tenant}/oauth2/v2.0/token` (client credentials, client ID and secret are sent):
* `/groups` only when an administrator searches for groups in the settings screen. The typed search text is sent.
* `/users/{id}/checkMemberGroups` during sign-in when the ID token carries no usable `groups` claim. The user's Microsoft object ID and the configured group IDs are sent; Microsoft returns which of those groups the user belongs to.
The plugin receives the user's e-mail address / user principal name, display name and Microsoft object ID from Microsoft and uses them solely to find the matching WordPress account. Nothing else is stored. The plugin receives the user's e-mail address / user principal name, display name and Microsoft object ID from Microsoft and uses them solely to find the matching WordPress account. Nothing else is stored.
Microsoft terms and privacy: [Microsoft Services Agreement](https://www.microsoft.com/servicesagreement), [Microsoft Privacy Statement](https://privacy.microsoft.com/privacystatement), [Microsoft identity platform documentation](https://learn.microsoft.com/entra/identity-platform/). Microsoft terms and privacy: [Microsoft Services Agreement](https://www.microsoft.com/servicesagreement), [Microsoft Privacy Statement](https://privacy.microsoft.com/privacystatement), [Microsoft identity platform documentation](https://learn.microsoft.com/entra/identity-platform/).
@ -86,6 +94,14 @@ Set the tenant to `consumers` or `common`. Note that Microsoft does not allow qu
The e-mail address in the Microsoft token does not match any WordPress user. Check the user's e-mail address in WordPress, enable the UPN fallback on the Security tab, or add the `email` optional claim in the app registration. The e-mail address in the Microsoft token does not match any WordPress user. Check the user's e-mail address in WordPress, enable the UPN fallback on the Security tab, or add the `email` optional claim in the app registration.
= How do I restrict sign-in to certain Entra groups? =
Open the Security tab, search for the groups (requires the Microsoft Graph application permission `GroupMember.Read.All` with admin consent) or paste their object IDs, add them and save. During sign-in the plugin first looks at the `groups` claim of the ID token; if the app registration does not emit one (or the user is in more than 200 groups) it asks Microsoft Graph (`User.Read.All`). If membership cannot be verified, the sign-in is refused.
= How do I get back in when button-only mode is on and Microsoft sign-in is broken? =
Open the fallback link shown on the Security tab (`wp-login.php?m365_fallback=KEY`); the password form is shown again in that browser for 30 minutes. Without the link, add `define( 'M365_LOGIN_DISABLE_BUTTON_ONLY', true );` to `wp-config.php` or rename the plugin folder via FTP.
= Does it work with custom login pages? = = Does it work with custom login pages? =
Yes, use the shortcode `[m365_login_button redirect="/dashboard/"]`. Yes, use the shortcode `[m365_login_button redirect="/dashboard/"]`.
@ -103,7 +119,8 @@ The settings, cached data and the per-user Microsoft object ID are removed.
1. The customised button on the WordPress login screen. 1. The customised button on the WordPress login screen.
2. Settings Connection tab with redirect URI and tenant test. 2. Settings Connection tab with redirect URI and tenant test.
3. Settings Button tab with live preview, colour pickers and icon picker. 3. Settings Button tab with live preview, colour pickers and icon picker.
4. Settings Security tab. 4. Settings Security tab with the Entra group picker and button-only mode.
5. Login screen in button-only mode.
== Changelog == == Changelog ==