wp-m365-login/CHANGELOG.md
Friederich Loheide cc88f145f2
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
Match accounts by mail address and user principal name
Sign-in and sync only used the first address (the mail attribute). A
WordPress account that carries the user principal name while the
Microsoft mail differs was not found at sign-in ("no WordPress account")
and the sync created a duplicate account for it.

Both now try the mail address and then the user principal name (UPN
only for members, only with the UPN fallback on, and only within the
e-mail domain allow-list). Privileged accounts keep the stricter rule;
a matching UPN is sufficient there as well.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 04:23:02 +00:00

65 lines
8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Changelog
All notable changes to this project are documented in this file. The format follows
[Keep a Changelog](https://keepachangelog.com/) and the plugin adheres to
[Semantic Versioning](https://semver.org/).
## [1.1.0] 2026-09-23
### Added
- User sync (new "User sync" tab): imports Microsoft 365 / Entra ID users as WordPress accounts the whole tenant or the (nested) members of selected groups, guests optional and links existing accounts by e-mail address.
- Default role plus group → role mapping with a Graph-powered group picker; mapped roles either added to or replacing the default role (first match wins, reorderable). Roles of accounts that existed before the sync are only managed on request.
- Selectable profile attributes (display name, first/last name, job title, department, company, office, employee ID, phones, address, language) and the profile photo, which replaces the Gravatar.
- Profile photos follow Microsoft 365 on every run: versions are compared via Graph `$batch` (20 users per request), changed photos are downloaded again (old file deleted, new URL), photos deleted in Microsoft 365 are deleted in WordPress. Errors never delete a photo.
- Deselected profile fields (`m365_*`) and photos are removed from the profiles on the next run; fields cleared in Microsoft 365 are cleared in WordPress; a user's photo is deleted together with the user.
- Deactivation or deletion of WordPress accounts whose Microsoft 365 account was disabled, deleted or removed from the sync groups; automatic reactivation. Deactivated accounts cannot sign in at all (Microsoft, password, application passwords) and lose all sessions.
- Safeguards: dry run, safety stop above 20 % deprovisioning (at least 5 accounts), abort on any Graph error, deletion only on a 404 for the object ID, protected pre-existing administrators and own account, content reassignment required for deletion, run lock.
- Scheduled sync via WP-Cron (hourly, twice daily, daily), `wp m365-login sync [--dry-run]`, report of the last run in the settings.
- "Microsoft 365" column and deactivate/reactivate row actions on the users screen; read-only Microsoft 365 section on the profile screen.
- Graph client: paging, retry on throttling (429/503/504), user, group member and photo endpoints.
- Filters and actions for the sync (`m365_login_sync_*`, `m365_login_user_disabled`, `m365_login_user_enabled`).
- Excluded Entra groups (Security tab): members can never sign in with Microsoft, even when they are in an allowed group. Checked via the `groups` claim and always via Microsoft Graph `checkMemberGroups` (a filtered claim cannot prove non-membership); fails closed.
### Changed
- The group picker is reusable (security groups, sync groups, role mapping).
- "Link Microsoft account" on the profile screen (the signed-in user binds their own Microsoft account) and an administrator-assigned Microsoft account (UPN) per user for administrators whose user principal name differs from their e-mail address. Sign-in finds bound accounts by object ID first, then by assigned UPN, then by e-mail.
- Privacy exporter and eraser for the data the plugin copies.
### Fixed
- Accounts whose WordPress e-mail is the user principal name (while the Microsoft mail differs) were not found at sign-in and got a duplicate account from the sync; sign-in and sync now try the mail address and the UPN.
### Security
Third audit (details: docs/security-audit.md, section 7): XML-RPC `system.multicall` bypass of button-only mode closed; privileged-account rules extended (bind_oid off, multisite-wide capabilities, code/HTML capabilities, deactivated administrators); cookie protection on WordPress 6.0/6.1; atomic and refreshed run lock with crash report; per-account tenant for deprovisioning; demotion safety stop; safety stop based on accounts linked before the run; photos re-encoded and removed on deactivation; one-time hardening of accounts deactivated by 1.0.
Fixes from a full second security audit (details: docs/security-audit.md, section 6):
- Multisite: settings, connection test, certificates and user sync require `manage_network_options` (a site admin could otherwise sign in as the super admin via an own tenant).
- Administrator accounts are only linked (sync and first sign-in) through a matching user principal name of a member account, never through the freely settable `mail` attribute; their e-mail address is never changed by the sync; e-mail change notifications stay on for other accounts.
- Button-only mode: exemption by credential (application passwords, WP-CLI) instead of request context closes bypasses through `xmlrpc.php` and REST login handlers of other plugins; API requests never receive login cookies; same message for right and wrong passwords; mode no longer switches off when the connection breaks; fallback cookie expires on the server; the correct fallback key works despite IP lockouts; right-most proxy header entry; start limit raised to 300.
- Multi-tenant mode refuses guest/external identities; one Microsoft object ID can only be bound to one WordPress account.
- Deactivation also sets a random password, revokes application passwords and removes the role (restored on reactivation); linked people disabled in Microsoft 365 are deactivated even if their e-mail vanished or changed domain; duplicate bindings are all deprovisioned.
- Sync safety: abort on an empty directory answer, no deprovisioning in the first run after a tenant change, atomic run lock, strict photo path validation.
- Certificates: key+certificate bundles are refused in the certificate field and only a clean re-exported certificate is stored.
- Robust sanitising of array input, encoded `redirect_to` in the button URL, separate nonces per AJAX action, escaped role lists, no Graph retries/sleeps during sign-in, warnings for public Microsoft 365 groups, group rules in multi-tenant mode and salts missing from wp-config.php, cached Graph token removed on uninstall.
### Fixed
- Failed Microsoft sign-ins (token exchange, token verification, object ID mismatch, group checks) ended in a PHP fatal error because the auth component's log helper had been removed in 1.0.0 development.
- "Generate certificate" and removing the certificate did not keep the change and encrypted a stored client secret a second time (internal settings writes ran through the form sanitiser).
## [1.0.0] 2026-09-22
### Added
- "Sign in with Microsoft" button on `wp-login.php` (OpenID Connect authorization code flow with PKCE).
- Matching of existing WordPress users by e-mail address (optional UPN fallback), no user provisioning.
- Settings screen (own top-level menu entry "M365 Login") with connection, button and security tabs, live button preview, colour presets, media-library icon picker, redirect-URI copy button and tenant connectivity test.
- ID token verification against Microsoft's JWKS (RS256, issuer, audience, tenant, expiry, nonce).
- Encrypted client secret storage (AES-256-GCM).
- Certificate based client authentication (RFC 7523 `private_key_jwt`): one-click generation of a 3072-bit RSA key pair with self-signed certificate, `.cer` download, own PEM upload, expiry display; step-by-step guides for both methods in the settings.
- Security audit (docs/security-audit.md) with fixes: unverified `email` claim ignored in multi-tenant mode, login-start rate limit, trusted proxy IP header.
- Account binding to the Microsoft object ID, e-mail domain allow-list.
- Entra group restriction with a Graph-powered group picker; membership verified via the `groups` claim or Microsoft Graph `checkMemberGroups`.
- Button-only mode that hides the password form and blocks password sign-in on `wp-login.php`, with a secret, rate-limited fallback link and a `wp-config.php` emergency constant.
- Custom login page support: automatic button in `wp_login_form()` forms, `m365_login_button()` / `m365_login_messages()` template functions, custom login URL for error messages, fallback link and logout redirect.
- `[m365_login_button]` shortcode (with `divider` and `messages` attributes) and developer hooks.
- German translation.