Add Microsoft 365 user sync with roles, profile fields and deprovisioning
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run

New "User sync" tab that imports Microsoft 365 / Entra ID users as
WordPress accounts and keeps them up to date:

- Scope: whole tenant or the (nested) members of selected groups,
  guests optional, e-mail domain allow-list respected. Existing accounts
  are linked by e-mail address.
- Roles: selectable default role plus a group -> role mapping (in
  addition to or instead of the default role, first match wins).
  Roles of pre-existing accounts are only managed on request.
- Profile: selectable Graph attributes (names, job title, department,
  phones, address, language, ...) and the profile photo as avatar.
- Deprovisioning: accounts disabled or deleted in Microsoft 365 (or
  removed from the sync groups) are deactivated or deleted; accounts
  deactivated by the sync are reactivated automatically. Deactivated
  accounts lose every sign-in path and all sessions.
- Safeguards: dry run, safety stop above 20 % (min. 5) deprovisioning,
  abort on any Graph error, "deleted" only on a 404 for the object ID,
  protected pre-existing administrators and own account, content
  reassignment required for deletion, run lock.
- Runs manually, via WP-Cron or `wp m365-login sync [--dry-run]`.
- Users screen column with deactivate/reactivate row actions and a
  read-only Microsoft 365 section on the profile screen.

The Graph client gains paging, retry on throttling and user, group
member and photo endpoints. The group picker is now reusable.
Version 1.1.0, German translations (du/Sie), docs and audit addendum.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-23 16:34:30 +00:00
parent 1708bae91a
commit 4edf20bc45
20 changed files with 5532 additions and 982 deletions

View file

@ -4,7 +4,7 @@ Tags: microsoft, entra id, azure ad, sso, login
Requires at least: 6.0
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 1.0.0
Stable tag: 1.1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@ -16,7 +16,8 @@ Adds a customisable "Sign in with Microsoft" button to the login page. Existing
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.
* **Sign-in never creates users.** 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.
* **Optional user sync.** Import all Microsoft 365 users (or the members of selected groups) as WordPress accounts, assign a default role plus extra roles through a group → role mapping, copy selected profile attributes and the profile photo, and deactivate or delete WordPress accounts whose Microsoft 365 account was disabled or deleted. Runs on demand, on a WP-Cron schedule or with `wp m365-login sync`; a dry run shows every change first.
* **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.
* **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).
@ -37,6 +38,8 @@ The plugin is deliberately small and strict:
* The **client secret / private key is encrypted at rest** (AES-256-GCM, key derived from your WordPress salts) and never displayed again.
* In multi-tenant mode the unverified `email` claim is ignored; matching uses the user principal name (verified domain) only.
* Login starts and fallback-key attempts are rate limited per client.
* The user sync stops before deactivating anything when a Microsoft Graph request fails, treats an account as deleted only when Graph returns 404 for its object ID, refuses to deactivate or delete more than 20 % of the linked accounts in one run, and never touches administrators that existed before the sync or your own account.
* Deactivated accounts lose every sign-in path (Microsoft, password, application passwords) and all sessions immediately.
* Every setting is sanitised, every output escaped, every admin request nonce- and capability-checked.
= Developer hooks =
@ -47,10 +50,12 @@ The plugin is deliberately small and strict:
* `m365_login_allow_user` filter, return `false` to block a matched user (e.g. group checks).
* `m365_login_success` action after a successful sign-in, receives the user and verified claims.
* `m365_login_block_password_login` filter, return `false` to exempt a password sign-in from button-only mode.
* `m365_login_sync_attributes`, `m365_login_sync_roles`, `m365_login_sync_new_user_data`, `m365_login_sync_email`, `m365_login_sync_protect_user`, `m365_login_sync_deprovision_limit`, `m365_login_sync_photo_limit`, `m365_login_sync_photo_interval` filters for the user sync.
* `m365_login_sync_user_created`, `m365_login_sync_finished`, `m365_login_user_disabled`, `m365_login_user_enabled` actions for the user sync.
== External services ==
This plugin connects to **Microsoft identity platform (Microsoft Entra ID)** to authenticate users. It is required for the plugin's only purpose signing users in with their Microsoft account and is only contacted when a user clicks the Microsoft button or when an administrator uses the "Test tenant" button.
This plugin connects to **Microsoft identity platform (Microsoft Entra ID)** to authenticate users. It is required for the plugin's main purpose signing users in with their Microsoft account and is only contacted when a user clicks the Microsoft button, when an administrator uses the "Test tenant" button, or when the optional user sync runs.
Endpoints used (all under `https://login.microsoftonline.com/`):
@ -59,12 +64,14 @@ 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}/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):
When the optional **group restriction** or the optional **user sync** is used, 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 or signed assertion 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.
* `/users`, `/groups/{id}/transitiveMembers`, `/users/{id}` only while the user sync runs (manually, on the configured schedule or via WP-CLI). The configured group IDs and the object IDs of linked accounts are sent; Microsoft returns the users with their account status and the profile attributes selected in the settings.
* `/users/{id}/photos/240x240`, `/users/{id}/photo` only while the user sync runs and "Profile photo" is selected. Returns the user's profile photo.
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.
For sign-in the plugin receives the user's e-mail address / user principal name, display name and Microsoft object ID and uses them solely to find the matching WordPress account. The user sync stores the object ID, the account status and the attributes selected by the administrator (for example name, job title, department, phone numbers, profile photo) in the WordPress user profile; profile photos are saved in `wp-content/uploads/m365-login-avatars/` and are shown publicly wherever WordPress displays avatars.
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/).
@ -88,7 +95,15 @@ Both work. A certificate is recommended: the private key stays on your server (e
= Does the plugin create users? =
No. Users must already exist in WordPress. The e-mail address is the only link between the Microsoft account and the WordPress account. This is intentional it keeps the administrator in control of who can access the site.
Signing in never creates users: they must already exist in WordPress, linked by e-mail address. If you want accounts for your Microsoft 365 users, enable the **User sync** tab: it imports all users (or the members of selected groups) ahead of time, assigns roles and keeps profiles up to date.
= What happens to people who leave the organisation? =
With the user sync, a WordPress account whose Microsoft 365 account is disabled or deleted can be deactivated (no sign-in of any kind, sessions ended) or deleted (content reassigned to a user you pick). If the Microsoft 365 account is enabled again, an account deactivated by the sync is reactivated automatically. Administrators that existed before the sync are never deactivated or deleted automatically.
= Which Microsoft Graph permissions does the user sync need? =
The application permission `User.Read.All` with admin consent, plus `GroupMember.Read.All` when you limit the sync to groups or map groups to roles. A tenant GUID must be configured on the Connection tab.
= Which accounts can sign in? =
@ -120,7 +135,7 @@ Yes. Settings are per site; a user must be a member of the site (or a super admi
= What happens on uninstall? =
The settings, cached data and the per-user Microsoft object ID are removed.
The settings, cached data, the sync report and schedule, stored profile photos and the per-user plugin data (Microsoft object ID, deactivation status) are removed. Imported accounts and copied profile fields (`m365_*` user meta) are kept. Deactivated accounts become active again, so delete them first if they must stay locked.
== Screenshots ==
@ -132,10 +147,20 @@ The settings, cached data and the per-user Microsoft object ID are removed.
== Changelog ==
= 1.1.0 =
* New: user sync import Microsoft 365 users (whole tenant or selected groups) with a default role and group → role mapping, selectable profile attributes and profile photos as avatars.
* New: deactivate or delete WordPress accounts whose Microsoft 365 account was disabled or deleted; automatic reactivation; dry run, safety stop and protected administrators.
* New: "Microsoft 365" column, deactivate/reactivate row actions and a read-only Microsoft 365 section on the profile screen.
* New: `wp m365-login sync [--dry-run]` WP-CLI command and scheduled sync via WP-Cron.
* Fix: generating or removing the certificate in the settings did not keep the change and broke a stored client secret.
= 1.0.0 =
* Initial release.
== Upgrade Notice ==
= 1.1.0 =
Adds an optional Microsoft 365 user sync (import, roles, profile fields, deactivation). Nothing changes until you enable it on the new User sync tab.
= 1.0.0 =
Initial release.