Uploaded image for project: 'Observium'
  1. Observium
  2. OBS-5322

Add native Microsoft Entra ID (Azure AD) SSO login option alongside local auth

    XMLWordPrintable

Details

    • New Feature
    • Resolution: Unresolved
    • Minor
    • None
    • None
    • Authentication
    • 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.

      Attachments

        Activity

          People

            landy Mike Stupalov
            rebeluca Luca Rebellato
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

            Dates

              Created:
              Updated: