Skip to content

Single Sign On

Ed Mozley edited this page Jun 27, 2026 · 4 revisions

Single Sign-On (SSO / OIDC)

FreeITSM supports single sign-on via OpenID Connect (OIDC), letting analysts β€” and self-service portal requesters β€” sign in through an external identity provider (IdP) instead of β€” or alongside β€” a local username and password.

It's a generic OIDC implementation: one code path works with any compliant provider, including Keycloak, Microsoft Entra ID (Azure AD), Okta, Auth0, Google Workspace and Authentik. You can configure several providers at once, which makes phased rollouts and side-by-side pilots straightforward (e.g. most users on Keycloak while a group trials Entra).

Worked example: see ↳ Setting up SSO with Keycloak (Docker) for a complete, copy-paste walkthrough using a local Keycloak instance.

Key ideas

  • Driven by discovery. Everything hangs off the provider's discovery document at <issuer>/.well-known/openid-configuration. You only supply a display name, issuer URL, client ID and client secret β€” the authorization, token, JWKS and logout endpoints are all read from discovery. That's why one form works for every IdP.
  • Local login stays. SSO sits next to local username/password login, which remains as a break-glass fallback. You can never be locked out by a misconfigured or offline IdP.
  • The analysts table is the identity store. An SSO login is mapped to an analyst row; permissions, ticket assignment, teams etc. all continue to work exactly as for local users.

How a sign-in works

  1. The login page offers an email-first box (type your email β†’ routed to your provider) plus a "Sign in with …" button per enabled provider.
  2. FreeITSM redirects the browser to the provider using the Authorization Code flow with PKCE (S256), carrying a state (CSRF protection) and nonce (replay protection).
  3. The user authenticates at the IdP (which also handles its own MFA).
  4. The provider redirects back to …/api/auth/oidc_callback.php. FreeITSM exchanges the code for tokens over a server-to-server back channel, then validates the ID token: signature against the provider's JWKS, plus issuer, audience, nonce and expiry.
  5. The identity is mapped to an analyst (see below), the session is established, and the user lands in the app.

JWT/JWKS validation uses the vendored firebase/php-jwt library (includes/vendor/firebase-jwt/) β€” the security-critical token checking is never hand-rolled.

Configuring a provider

Go to System β†’ Single Sign-On.

Global settings

  • Enable single sign-on β€” the master switch. Turning it off instantly reverts everyone to local login (break-glass kill switch).
  • Allow local login β€” keep the local username/password form available. Leave on unless you want an SSO-only experience.

Redirect URI

The page shows the exact redirect URI to register in your IdP (e.g. http://localhost/freeitsm-app/api/auth/oidc_callback.php). Copy it into the provider's valid redirect URIs list β€” it's where the IdP sends users back after they authenticate.

Adding a provider

Click Add and fill in:

Field What it is
Display name The label on the login button, e.g. Sign in with Keycloak
Issuer URL The provider's base URL (see Issuer URLs below). The Test button validates it by fetching the discovery document.
Client ID The client/application identifier created in the provider
Client secret The client's secret β€” stored encrypted (AES-256-GCM) and never returned to the browser
Scopes OIDC scopes; default openid email profile
Enabled Show this provider's button on the login page
Auto-create users (JIT) Create an analyst automatically on first login (see below)
Default module access Modules granted to JIT-created users β€” blank means full access, so set this for pilots

Account mapping & provisioning

On a successful sign-in, FreeITSM resolves the analyst in this order:

  1. Existing identity link β€” a stored link between the provider and the IdP's sub (subject) claim.
  2. Email match β€” an active analyst whose email matches the token's (the email must be verified at the IdP). The identity is then linked for next time.
  3. Just-in-time (JIT) provisioning β€” if the provider has Auto-create users enabled, a new analyst is created (with an unusable local password), assigned to that provider, and granted the provider's default modules.

If none apply (no match and JIT is off), the sign-in is rejected with a readable message.

Strict isolation

An analyst may only sign in via the provider they're assigned to (analysts.auth_provider_id). An SSO login can never silently take over a local account or an account assigned to a different provider. This keeps parallel pilots cleanly separated.

Assigning existing users to a provider

JIT only assigns brand-new users. To move existing analysts onto SSO, open Tickets β†’ Settings β†’ Analysts, edit the analyst, and set Sign-in method to the provider. On their next SSO sign-in their identity is linked automatically (matched by email).

Break-glass & SSO-only mode

  • When SSO is active the login page leads with email-first + provider buttons; the local form is tucked behind a "Sign in with a local account" link.
  • Setting Allow local login to off hides that link too (a clean SSO-only look), but local auth is never hard-disabled: the URL …/login.php?local=1 and the master kill switch always restore local login, so no one can be locked out.
  • MFA: SSO users authenticate (and do MFA) at the identity provider, so FreeITSM skips its own local TOTP step for them.

Single logout

Signing out of FreeITSM also ends the session at the identity provider via its end_session_endpoint, so the user isn't silently re-authenticated on the next visit.

Security notes

  • Authorization Code + PKCE (S256), with state and nonce validation.
  • ID-token signature verified against the provider's JWKS; issuer, audience, nonce and expiry all checked.
  • Client secrets encrypted at rest and never sent to the browser (the config UI shows only whether a secret is stored).
  • The email-resolver endpoint returns "local" for unknown emails, so it doesn't reveal whether an account exists.

Self-service portal SSO

SSO isn't just for analysts β€” the same providers can sign requesters into the self-service portal too. The portal login page gets the identical experience as the analyst login: an email-first box (type your email β†’ routed to your provider), a "Sign in with …" button per enabled provider, and a local-account break-glass. Turning Allow local login off β€” and the ?local=1 URL β€” behave exactly as on the analyst login.

Under the hood there's still one OIDC flow and one callback, made portal-aware: oidc_login.php, oidc_callback.php and resolve_login.php carry a portal=self-service flag (stashed in the session so the single IdP redirect URI serves both portals). On the way back, the callback resolves against the requester users table β€” not analysts β€” sets the self-service session, and lands the user in the portal (self-service/index.php).

Account mapping for requesters

Requester mapping mirrors the analyst flow one layer down, with one deliberate difference:

  1. Existing identity link β€” a stored link between the provider and the IdP's sub (subject) claim.
  2. Email match β€” a requester whose email matches the token's (the email must be verified at the IdP).
  3. Just-in-time (JIT) provisioning β€” if the provider has Auto-create users enabled, a brand-new requester is created (passwordless) and assigned to the provider.

The difference: because requesters self-onboard (a ticket-created contact starts passwordless and unassigned), a requester matched by verified email who is still unassigned is auto-claimed onto the provider on first sign-in. Analysts, being privileged, still require explicit enrolment β€” they're never auto-claimed. An already-assigned requester is still strictly isolated to their own provider.

Separate identity spaces

Analysts and requesters are separate identity spaces, so the same email resolves independently per portal β€” a person can be an analyst on one provider and a requester on another (or neither). Single logout works on the portal too: signing out of self-service also ends the session at the IdP.

Database (self-service layer)

These mirror the analyst SSO tables one layer down:

  • user_sso_identities β€” links a requester (users row) to their identity at a provider (the sub claim), unique per (provider, subject).
  • users.auth_provider_id β€” which provider a requester is assigned to (NULL = local password / unassigned).

Multi-company (MSP) SSO routing

On a multi-company (MSP) install, a provider can be owned by a client company (auth_providers.tenant_id; NULL = global / MSP-internal, serving every company). This lets each client company route its own requesters to its own IdP.

Email-first only on the portal

When more than one company exists, the portal login is email-first only β€” no provider buttons up front, because showing every client's "Sign in with …" button would leak every client's IdP to everyone. You type your email and the resolver routes by email domain β†’ company β†’ that company's enabled IdP(s), reusing the existing tenant_domains / tenant_sender_addresses routing (via a resolveTenantIdForAddress() helper). The company may have:

IdPs for the company What happens
0 Local password (fall through to the local form)
1 Routed straight through to that IdP
2+ A tenant-scoped picker β€” "Choose how to sign in" β€” for a company mid-migration between, say, Entra and Okta

A per-user pin (users.auth_provider_id) routes a returning requester straight to their provider, so the picker is a once-only first-login step.

Analyst login stays global

The analyst login only ever shows global providers (tenant_id IS NULL) β€” a client's IdP never appears there.

Invisible at N=1

This whole layer is gated on isMultiTenant(): a single-company install keeps the original buttons-up-front behaviour and is completely unaffected.

Note: this routes per company using each provider's own credentials (the working handshake β€” each client registers FreeITSM as a client app in their own IdP). The credential-free "MSP multi-tenant app + consent" onboarding model is designed but not yet built.

Providers list: Company column

On a multi-company install, System β†’ Single Sign-On shows an extra Company column in the providers table β€” the client company a provider is pinned to, or Global for an MSP-internal / all-companies provider. It makes it obvious at a glance why (e.g.) a client's IdP doesn't appear on the analyst login. get_sso_providers.php returns each provider's tenant_name (LEFT JOIN tenants), and the edit dialog gains a Company dropdown to set the owner (get / save_sso_provider carry tenant_id).

The column is hidden at N=1 (single company), consistent with the rest of multi-tenancy. The issuer column truncates with an ellipsis and shows the full issuer URL on hover (a title tooltip).

Issuer URLs by provider

Provider Issuer URL pattern
Keycloak https://<host>/realms/<realm>
Microsoft Entra ID https://login.microsoftonline.com/<tenant-id>/v2.0
Okta https://<org>.okta.com (or an /oauth2/<server> auth server)
Auth0 https://<tenant>.<region>.auth0.com/
Google Workspace https://accounts.google.com
Authentik https://<host>/application/o/<app-slug>/

Provider-specific concepts (Entra tenant + app registration, Keycloak realm, Okta org) all collapse into the issuer URL plus the client ID/secret β€” there's nothing else provider-specific to enter.

Database

  • auth_providers β€” one row per configured IdP (display name, issuer, client ID, encrypted secret, scopes, enabled, JIT toggle, default modules).
  • analyst_sso_identities β€” links an analyst to their identity at a provider (the sub claim), unique per (provider, subject).
  • analysts.auth_provider_id β€” which provider an analyst is assigned to (NULL = local password user).
  • auth_providers.tenant_id β€” on a multi-company install, the client company that owns a provider (NULL = global / MSP-internal). See Multi-company (MSP) SSO routing.
  • user_sso_identities / users.auth_provider_id β€” the self-service requester equivalents of the two analyst tables above. See Self-service portal SSO.

Related

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally