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 "Excluded Entra groups" card on the Security tab. Members of these groups (nested memberships count) can never sign in with Microsoft, even if they are in an allowed group. A hit in the ID token's groups claim refuses immediately. Otherwise the plugin always asks Microsoft Graph (checkMemberGroups), because a groups claim can be filtered in the app registration and cannot prove non-membership. Graph errors refuse the sign-in (fail closed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
170 lines
16 KiB
Text
170 lines
16 KiB
Text
=== M365 Login ===
|
||
Contributors: friloo
|
||
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.1.0
|
||
License: GPLv2 or later
|
||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||
|
||
Adds a customisable "Sign in with Microsoft" button to the login page. Existing users sign in with their Microsoft 365 / Entra ID account, matched by e-mail address.
|
||
|
||
== Description ==
|
||
|
||
**M365 Login** lets your existing WordPress users sign in with their Microsoft 365 (Microsoft Entra ID, formerly Azure AD) work or school account. It adds a button to the standard WordPress login screen and uses the OpenID Connect authorization code flow with PKCE.
|
||
|
||
The plugin is deliberately small and strict:
|
||
|
||
* **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).
|
||
* **Excluded groups.** Members of the groups you exclude can never sign in with Microsoft, even if they are in an allowed group (checked with Microsoft Graph, fails closed).
|
||
* **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.
|
||
* **Custom login pages.** The button is added to every `wp_login_form()` form automatically; a shortcode and a template function cover page builders and theme templates. Point the plugin at your login page and error messages, the fallback link and the post-logout redirect go there instead of wp-login.php.
|
||
|
||
= Security =
|
||
|
||
* OpenID Connect **authorization code flow with PKCE (S256)** – tokens are exchanged server-to-server and never pass through the browser.
|
||
* **Single-use state and nonce** values, bound to the browser with an HttpOnly, SameSite cookie (CSRF and replay protection).
|
||
* 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.
|
||
* **Account binding**: on first sign-in the immutable Microsoft object ID is stored with the user; later sign-ins with the same e-mail but a different Microsoft identity are refused.
|
||
* Optional **e-mail domain allow-list** and **group allow-list** (fails closed when membership cannot be verified).
|
||
* **Button-only mode** blocks password sign-in server-side, not just visually; the fallback key is rate limited and never stored in a cookie.
|
||
* **Certificate authentication** (RFC 7523 client assertion) as an alternative to a client secret: generate a 3072-bit key pair in the settings, upload only the public certificate to Entra ID. Nothing secret is ever transmitted.
|
||
* The **client secret / private key is encrypted at rest** (AES-256-GCM, key derived from your WordPress salts) and never displayed again.
|
||
* In multi-tenant mode the unverified `email` claim is ignored; matching uses the user principal name (verified domain) only.
|
||
* Login starts and fallback-key attempts are rate limited per client.
|
||
* 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 =
|
||
|
||
* `m365_login_show_button` – filter, hide the button conditionally.
|
||
* `m365_login_authorize_params` – filter the parameters sent to Microsoft (e.g. `domain_hint`).
|
||
* `m365_login_match_email` – filter the e-mail address used for the lookup.
|
||
* `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 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/`):
|
||
|
||
* `/{tenant}/oauth2/v2.0/authorize` – the user's browser is redirected here to sign in at Microsoft. Microsoft receives the application (client) ID, the redirect URI of this site, a random state, nonce and PKCE challenge.
|
||
* `/{tenant}/oauth2/v2.0/token` – the server exchanges the authorization code for an ID token. Microsoft receives the client ID, client secret, the code and the PKCE verifier.
|
||
* `/{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** 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, and on every sign-in when excluded groups are configured. 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.
|
||
* `/$batch` with `/users/{id}/photo`, and `/users/{id}/photos/240x240`, `/users/{id}/photo` – only while the user sync runs and "Profile photo" is selected. Returns the version and, when it changed, the image of the user's profile photo.
|
||
|
||
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/).
|
||
|
||
== Installation ==
|
||
|
||
1. Upload the plugin folder to `/wp-content/plugins/` or install it through the WordPress plugin screen, then activate it.
|
||
2. Open the new **M365 Login** menu entry and copy the **Redirect URI** shown in the sidebar.
|
||
3. In the [Microsoft Entra admin center](https://entra.microsoft.com/) open **App registrations → New registration**. Choose *Accounts in this organizational directory only*, select the **Web** platform and paste the redirect URI.
|
||
4. From the app's overview page copy the **Application (client) ID** and the **Directory (tenant) ID** into the plugin settings.
|
||
5. Pick the authentication method: either generate a certificate in the plugin and upload the downloaded `.cer` under **Certificates & secrets → Certificates**, or create a client secret under **Certificates & secrets → Client secrets** and paste its *value*. Both methods have a step-by-step guide in the settings.
|
||
6. Under **Token configuration** add the optional claim **email** for ID tokens (recommended). The delegated permissions `openid`, `profile` and `email` are granted by default.
|
||
7. Save. The button now appears on `wp-login.php`. Customise it on the **Button** tab.
|
||
|
||
Make sure every user who should be able to sign in has the same e-mail address in WordPress as in Microsoft 365.
|
||
|
||
== Frequently Asked Questions ==
|
||
|
||
= Client secret or certificate? =
|
||
|
||
Both work. A certificate is recommended: the private key stays on your server (encrypted), only the public certificate is uploaded to Entra ID, and every token request is signed with a short-lived assertion instead of sending a shared secret. The plugin generates the certificate for you and shows its expiry date.
|
||
|
||
= Does the plugin create users? =
|
||
|
||
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? =
|
||
|
||
Any Microsoft account of the configured tenant whose e-mail address (or user principal name) matches an existing WordPress user. Restrict it further with the e-mail domain allow-list or the `m365_login_allow_user` filter.
|
||
|
||
= Can I use it with personal Microsoft accounts (outlook.com)? =
|
||
|
||
Set the tenant to `consumers` or `common`. Note that Microsoft does not allow query strings in redirect URIs for apps that support personal accounts, so your site must use pretty permalinks (the callback URL is then `/m365-login/callback` without a query string).
|
||
|
||
= The sign-in fails with "No WordPress account exists for your Microsoft e-mail address" =
|
||
|
||
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? =
|
||
|
||
Yes. Forms rendered with `wp_login_form()` get the button automatically. For page builders use the shortcode `[m365_login_button redirect="/dashboard/" divider="yes"]`, in theme templates call `m365_login_button()`. Enter the page's URL on the Button tab so error messages and the fallback link point there. Button-only mode blocks password sign-ins from custom forms as well.
|
||
|
||
= Does it support multisite? =
|
||
|
||
Yes. Settings are per site; a user must be a member of the site (or a super admin) to sign in.
|
||
|
||
= What happens on uninstall? =
|
||
|
||
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 ==
|
||
|
||
1. The customised button on the WordPress login screen.
|
||
2. M365 Login – Connection tab with redirect URI and tenant test.
|
||
3. M365 Login – Button tab with live preview, colour pickers and icon picker.
|
||
4. M365 Login – Security tab with the Entra group picker and button-only mode.
|
||
5. Login screen in button-only mode.
|
||
|
||
== 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: profile fields and photos follow Microsoft 365 on every run – changed photos are replaced, deleted photos and cleared or deselected fields are removed.
|
||
* 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.
|
||
* New: excluded Entra groups – their members can never sign in with Microsoft.
|
||
* Fix: failed Microsoft sign-ins (e.g. expired secret, group not allowed) ended in a PHP fatal error instead of the error message.
|
||
* 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.
|