-
Notifications
You must be signed in to change notification settings - Fork 15
Single Sign On
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.
-
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
analyststable 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.
- The login page offers an email-first box (type your email β routed to your provider) plus a "Sign in with β¦" button per enabled provider.
- FreeITSM redirects the browser to the provider using the Authorization Code flow with PKCE (S256), carrying a
state(CSRF protection) andnonce(replay protection). - The user authenticates at the IdP (which also handles its own MFA).
- 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. - 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.
Go to System β Single Sign-On.
- 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.
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.
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 |
On a successful sign-in, FreeITSM resolves the analyst in this order:
-
Existing identity link β a stored link between the provider and the IdP's
sub(subject) claim. - 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.
- 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.
- An explicit
email_verified: falseclaim from the IdP is always refused. - Providers that omit the claim entirely are accepted by default, so sign-in works out of the box β Okta's org authorization server never sends it, while Keycloak and Entra do.
- A per-provider Require a verified-email claim toggle (off by default) lets admins demand an explicit
email_verified: trueβ useful for IdPs where users can self-register with unverified addresses.
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.
JIT only assigns brand-new users. To move existing analysts onto SSO, open System β 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).
- 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=1and 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.
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.
- Authorization Code + PKCE (S256), with
stateandnoncevalidation. - 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.
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).
Requester mapping mirrors the analyst flow one layer down, with one deliberate difference:
-
Existing identity link β a stored link between the provider and the IdP's
sub(subject) claim. - Email match β a requester whose email matches the token's (the email must be verified at the IdP).
- 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.
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.
These mirror the analyst SSO tables one layer down:
-
user_sso_identitiesβ links a requester (usersrow) to their identity at a provider (thesubclaim), unique per(provider, subject). -
users.auth_provider_idβ which provider a requester is assigned to (NULL= local password / unassigned).
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.
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.
The analyst login only ever shows global providers (tenant_id IS NULL) β a client's IdP never appears there.
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 (one MSP-owned app that clients consent to, keyed on the token's tenant claim) is designed but not yet built β the design lives in
docs/design/in the repository.
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).
| 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.
-
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 (thesubclaim), 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.
- β³ Setting up SSO with Keycloak (Docker) β full worked example
- Security β the wider authentication, encryption and brute-force defences
- System β other system-administration settings
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)