Four-part audit (OIDC/JWT/crypto, user sync, admin UI, login bypasses)
with dynamic PoCs against a real WordPress install; every fix is covered
by a regression test. Report: docs/security-audit.md, section 6.
Critical/High
- Multisite: settings, AJAX actions and certificate download require
manage_network_options (site admins could sign in as super admin).
- Privileged accounts are only linked (sync and first sign-in) via a
matching UPN of a member account, never via the settable mail
attribute; the sync never changes their e-mail address; e-mail change
notifications stay on.
- Button-only mode exempts by credential (application passwords, WP-CLI)
instead of request context, closing bypasses through xmlrpc.php and
REST login handlers; API requests never receive login cookies.
- Multi-tenant mode refuses guest/external identities.
Medium/Low
- Same message for right and wrong passwords; button-only no longer
switches off when the connection breaks; server-side fallback cookie
expiry; correct fallback key beats IP lockouts; right-most proxy hop;
higher start limit; one object ID per account.
- Deactivation sets a random password, revokes application passwords and
removes the role (restored on reactivation); disabled people are
deactivated even when their mail vanished; duplicate bindings handled.
- Sync: abort on empty directory answer, no deprovisioning right after a
tenant change, atomic run lock, strict photo path validation.
- Certificates: key bundles refused, clean re-exported certificate.
- Array-safe sanitising, encoded redirect_to, per-action nonces, escaped
role lists, no Graph sleeps during sign-in, warnings for public groups,
multi-tenant group rules and missing salts, uninstall clears the token.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Photo versions are compared on every sync run via Graph $batch
(20 users per request); only changed photos are downloaded, the old
file is deleted and the avatar URL changes. Photos deleted in
Microsoft 365 are deleted in WordPress. Graph errors never delete a
photo. Download limit per run (500) with deferral to the next run.
- Switching the photo sync off removes all stored photos; deselected
m365_* profile fields are removed from the profiles.
- A user's photo is deleted together with the user (delete_user hook).
- Dry run reports photo changes without downloading.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>