Details
-
New Feature
-
Resolution: Unresolved
-
Minor
-
None
-
None
-
None
Description
Summary
Adds an optional "Sign in with Microsoft" button on the logon page, implementing OpenID Connect (Authorization Code + PKCE) against Microsoft Entra ID, as an additional login path alongside the existing mysql auth_mechanism - not a replacement. No change to auth_mechanism, so permissions/roles/API tokens/user management all keep working unchanged for both local and SSO users. Built and tested on a self-hosted instance (SVN trunk) as a local patch; posting here in case upstream is interested in a PR, and to document the approach/gotchas for anyone else attempting it.
Design
- auth_mechanism stays "mysql"; the new login path is layered on top.
- Entra-authenticated users are stored as ordinary users rows with type='mysql' (same pattern cas.inc.php already uses for CAS) - necessary because every permission/role lookup dispatches through mysql_auth_user_level() etc., filtered by type='mysql'.
- New accounts auto-provision on first login with a random, unusable local password (can_modify_passwd=0).
- An Entra app-role claim maps to an Observium level via $config['auth_entra_role_map']. No mapped role -> level 0 (Disabled), zero-trust default.
- Full RS256 signature verification of the id_token against Entra's published JWKS (issuer/audience/expiry/nonce checked), PKCE (S256) on the code exchange. No third-party libraries - JWK->PEM is a small hand-rolled ASN.1 DER builder, verified against a self-generated test RSA keypair.
- Login finalization deliberately avoids touching authenticate.inc.php (the core login state machine); it duplicates its small "successful login" tail instead - see the gotcha below for why that needs care.
New files
- html/includes/authentication/entra-oidc.inc.php - OIDC helper library: discovery/JWKS (cached), PKCE, authorize-URL builder, token exchange (via the existing get_http_request() so proxy settings are respected), JWK->PEM, id_token verification, role mapping, user provisioning, session finalization.
- html/entra-login.php - front controller, builds state/nonce/PKCE, redirects to Microsoft.
- html/entra-callback.php - OIDC callback: validates state, exchanges code, verifies token, provisions user, finalizes session. Every failure path reuses the existing session_logout() + reauth_with_message() pair.
Changed files
- html/pages/logon.inc.php - adds the button (shown only when auth_entra_enabled).
- includes/defaults.inc.php - new defaults block (auth_entra_* settings, role map example).
- config.php - operator fills in tenant ID / client ID / client secret.
Entra-side setup
App registration + redirect URI https://<host>/entra-callback.php, client secret, App roles (e.g. Observium.Admin, Observium.Read) created and assigned to users/groups in the Enterprise Application, no extra Graph permissions needed.
Implementation gotcha worth documenting
The OIDC/JWT plumbing worked on the first try. The real bug was session handling: session_set_var() opens/writes/closes the PHP session on every call; a raw $_SESSION[...]= write made between two session_set_var() calls is silently discarded the next time session_set_var() reopens the session (it reloads from storage). This wiped userlevel/auth_password_fingerprint right after a successful Entra login, logging the user straight back out on the next page load ("session password fingerprint missing"). Fixed by mirroring authenticate.inc.php's own ordering: session_write_close() right after auth_session_refresh() succeeds, before touching requesttoken. Flagging this for anyone else touching session code near auth.
Known limitations
- Logout is local-only (no Entra end_session_endpoint redirect).
- "Remember me" doesn't apply to SSO sessions.
- auth_entra_sync_profile overwrites local level/email/realname from Entra on every login by design.
Status
php -l clean on all files; JWT verification unit-tested against a self-generated RSA keypair (valid sig accepted, tampered payload rejected, wrong-key rejected); live-tested end-to-end against a real Entra tenant, working.
Happy to open a PR if there's interest - let me know the preferred base branch and whether Entra credentials should live in config.php (as here) or be settable from /settings/ like other auth backends.