OIDC Authentication
Simple-PHP-IPAM supports single sign-on via OpenID Connect (OIDC) Authorization Code + PKCE flow. Any compliant IdP works: Google, Microsoft Entra ID (Azure AD), Okta, Keycloak, Auth0, Authentik, Dex, and others.
OIDC is implemented in pure PHP using only the built-in openssl extension — no Composer packages required.
v2.7.0 note — OIDC is fully wired to the database.
oidc_enabled(), the login button, the authorize redirect, the callback token exchange, and every claim-mapping flag all read throughipam_setting('oidc.*'). Configure everything from ⚙ Admin → Settings → OIDC / SSO — edits take effect on the next login with noconfig.phpchange and no restart. Theconfig.phpfallback continues to work in current v3.x releases for installs that have not yet migrated their values into the database. New in v2.7.0:oidc.disable_local_login,oidc.disable_emergency_bypass, andoidc.hide_emergency_linkare now registered database settings you can toggle from the admin UI.
Contents
- How it works
- Prerequisites
- Configuration
- IdP setup examples
- User provisioning and linking
- Disabling local login
- Troubleshooting
How it works
- User clicks Sign in with <display_name> on the login page
- Browser is redirected to the IdP with a PKCE code challenge, state, and nonce
- User authenticates at the IdP
- IdP redirects back to
oidc_callback.phpwith an authorization code - The callback exchanges the code for an ID token (verifying the PKCE code verifier)
- The ID token signature is verified against the IdP's JWKS (RS256/RS384/RS512)
- The
subclaim is matched to a local user account; ifauto_provisionis enabled a new account is created if none exists - The user is logged in with the same session mechanism as local auth
The discovery document and JWKS are cached in data/tmp/ for one hour. A single automatic JWKS cache-bust is attempted if signature verification fails, to handle in-flight key rotation.
Prerequisites
- PHP 8.2+ with
opensslextension (standard on most hosts) - HTTPS — OIDC callbacks must be served over HTTPS
allow_url_fopen = Oninphp.ini(used for discovery and JWKS fetches)- An OIDC client registered with your IdP (see IdP setup examples)
Configuration
Configure OIDC from ⚙ Admin → Settings → OIDC / SSO in the web UI. As of v2.7.0, every key listed below is a database setting; edits take effect on the next login with no restart. The config.php snippet shown is provided for reference and as a fallback for installs that have not yet migrated values into the settings table.
'oidc' => [
'enabled' => true,
'display_name' => 'Okta', // Label on the login button
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret',
'discovery_url' => 'https://your-org.okta.com/oauth2/default',
'redirect_uri' => 'https://ipam.example.com/oidc_callback.php',
'scopes' => 'openid email profile',
'auto_link' => true,
'auto_provision' => false,
'default_role' => 'readonly',
'disable_local_login' => false,
'hide_emergency_link' => false,
'disable_emergency_bypass' => false,
],
Settings
| Key | Required | Description |
|---|---|---|
enabled | yes | Set to true to activate OIDC |
display_name | no | Button label on the login page (default: SSO) |
client_id | yes | OAuth 2.0 client ID from your IdP |
client_secret | yes | OAuth 2.0 client secret from your IdP |
discovery_url | yes | IdP base URL — /.well-known/openid-configuration is appended automatically, or supply the full path |
redirect_uri | yes | Must match exactly what is registered with the IdP |
scopes | no | Space-separated scopes (default: openid email profile) |
auto_link | no | Link an incoming OIDC login to an existing unlinked local account by preferred_username then email (default: false). Implied by auto_provision. |
auto_provision | no | Create a local user on first OIDC login if none exists (default: false). Enabling this also flips auto_link on. |
default_role | no | Role assigned to auto-provisioned users: admin, netops, or readonly (default: readonly). The netops value was added to the dropdown in v2.11.0 (#501) — prior releases only offered admin / readonly even though the schema and demo seed already carried a netops role. |
disable_local_login | no | Hide the password form when OIDC is enabled (default: false). See Disabling local login |
hide_emergency_link | no | Hide the ?local=1 link text on the SSO-only login page (default: false). The URL still works. Database-backed since v2.7.0. |
disable_emergency_bypass | no | Make ?local=1 completely non-functional (default: false). Database-backed since v2.7.0. |
IdP setup examples
- Go to Google Cloud Console → APIs & Services → Credentials
- Create an OAuth 2.0 Client ID (type: Web application)
- Add
https://ipam.example.com/oidc_callback.phpto Authorized redirect URIs - Copy the client ID and secret
'discovery_url' => 'https://accounts.google.com',
Microsoft Entra ID (Azure AD)
- Azure Portal → App registrations → New registration
- Set redirect URI to
https://ipam.example.com/oidc_callback.php(platform: Web) - Under Certificates & secrets, create a new client secret
- Note your tenant ID
'discovery_url' => 'https://login.microsoftonline.com/{tenant-id}/v2.0',
Okta
- Okta Admin Console → Applications → Create App Integration (OIDC, Web Application)
- Add
https://ipam.example.com/oidc_callback.phpto Sign-in redirect URIs - Copy the client ID and secret
'discovery_url' => 'https://your-org.okta.com/oauth2/default',
Keycloak
- Keycloak Admin → Realm → Clients → Create
- Set Valid Redirect URIs to
https://ipam.example.com/oidc_callback.php - Enable Client authentication (confidential client)
'discovery_url' => 'https://keycloak.example.com/realms/your-realm',
Authentik
- Authentik Admin → Applications → Providers → Create OAuth2/OpenID Connect Provider
- Set redirect URI to
https://ipam.example.com/oidc_callback.php
'discovery_url' => 'https://authentik.example.com/application/o/your-app',
User provisioning and linking
Manual linking (recommended for existing installs)
By default, auto_provision is false. An admin must create or link accounts before any user can sign in via SSO.
Option 1 — Link via the admin UI:
- Create or locate the local user account in Admin → Users
- In the user's Actions panel, paste the IdP
subclaim value into the Link SSO field and submit - The account is now linked; the user can sign in via SSO on their next visit
Option 2 — Temporary auto_provision:
Enable auto_provision briefly, have the user sign in once via OIDC, then disable it again. The sub claim is stored and subsequent logins work without provisioning.
Auto-provisioning
With auto_provision = true:
- On the first OIDC login, the
subclaim is looked up inusers.oidc_sub— no match found - An existing unlinked account is sought: first by matching
preferred_username, then by matchingemail(against both theusernameandemailcolumns) - If a match is found, the account is automatically linked to the
subclaim and name/email are populated from the ID token if they were blank - If no match is found, a new account is created:
- Username derived from
preferred_usernameclaim →emaillocal-part →sub(fallback) - Name and email populated from the
nameandemailclaims - Unusable random password (account cannot be used for local auth)
- Role set to
default_role(default:readonly)
- Username derived from
Auto-provisioned accounts cannot log in with local credentials (the password is a random bcrypt hash). If OIDC becomes unavailable, an admin can set a proper password via Admin → Users → Reset PW.
Name and email sync
On every OIDC login, if a user's Name or Email fields are blank, they are silently populated from the name and email claims in the ID token. Fields that have already been set are not overwritten — you can always edit them manually in Admin → Users.
Unlinking an account
Admins can remove the OIDC link from any user in Admin → Users by clicking Unlink SSO. The local account remains active; the user can log in with their local password if one is set.
Disabling local login
Set 'disable_local_login' => true in the oidc config block to hide the username/password form on the login page. Users will only see the SSO button.
'oidc' => [
'enabled' => true,
// ... other settings ...
'disable_local_login' => true,
],
Emergency access: Even with disable_local_login enabled, local login is accessible at login.php?local=1 by default. Keep at least one active local admin account as a break-glass fallback.
Hiding the emergency link
If you want SSO-only appearance without advertising the bypass URL, enable oidc.hide_emergency_link from ⚙ Admin → Settings → OIDC / SSO (database-backed since v2.7.0). For back-compat, the same key in config.php works as a fallback:
'hide_emergency_link' => true,
The ?local=1 URL still works — the link text is simply not shown on the login page.
Disabling emergency access entirely
To make login.php?local=1 completely non-functional, enable oidc.disable_emergency_bypass from ⚙ Admin → Settings → OIDC / SSO (database-backed since v2.7.0). For back-compat, the same key in config.php works as a fallback:
'disable_emergency_bypass' => true,
Warning: If your IdP becomes unavailable while
disable_emergency_bypassistrue, no one can log in. Only set this after verifying your IdP uptime and having an out-of-band admin recovery procedure (e.g. SSH access to reset the DB).
Bot mitigation on the SSO login page
OIDC and local login share the same login page. The login_protection config block applies to both flows — if a CAPTCHA widget is configured, it is rendered before the SSO button as well as the password form. This protects the OIDC redirect initiation endpoint from automated abuse.
For Google reCAPTCHA Enterprise users, set recaptcha_enterprise.enabled = true in config.php and configure expected_action to match the action your reCAPTCHA assessment expects:
'recaptcha_enterprise' => [
'enabled' => true,
'project_id' => 'your-gcp-project',
'api_key' => 'your-api-key',
'expected_action' => 'login', // must match recaptcha_action below
],
'recaptcha_action' => 'login', // action name sent by app.js at runtime
See recaptcha_enterprise for the full config reference.
Troubleshooting
All OIDC errors are written to PHP's error log. The login page shows only a generic "SSO authentication failed" message to avoid leaking configuration details.
| Symptom | Likely cause |
|---|---|
| "Could not reach the identity provider" | discovery_url is wrong or unreachable; check allow_url_fopen |
| "SSO authentication failed" after redirect | Check the PHP error log for the specific reason |
| "state mismatch" in error log | Session lost between oidc_login.php and oidc_callback.php — check session cookie settings |
| "No matching RSA JWK" | The IdP uses a key kid not in its published JWKS, or JWKS cache is stale |
| "No local user found for sub=..." | auto_provision is false and no account has been linked to this sub |
| ID token expired | Large clock skew between your server and the IdP — sync server time via NTP |
| Redirect URI mismatch error from IdP | redirect_uri in config.php must exactly match the value registered with the IdP |
Password-manager autofill on the OIDC settings tab
Through v3.27.x, the OIDC config inputs on settings.php?tab=authentication#group-oidc may be autofilled by browser password managers (most notably LastPass) with credentials saved for your IPAM hostname. The fields look like a credential form to the manager's heuristic — sibling text + password inputs in the same <form> — and stored hints (autocomplete=off, data-1p-ignore, data-lpignore, data-bwignore) are ignored when the heuristic match is strong enough (see #1137).
Risk: an admin who clicks Save without noticing the autofill can overwrite a working OIDC config with the wrong values and lock themselves out of SSO.
Workaround for v3.27.x: add your IPAM hostname to your password manager's Never-URLs / blocked-sites list before editing OIDC config:
- LastPass — Account Settings → URL Rules → Never URLs → add
https://<your-ipam-host>/ - 1Password — Settings → Autofill → Sites with autofill disabled → add
<your-ipam-host> - Bitwarden — Account Settings → Display → Don't show this URL → add
<your-ipam-host>
Permanent fix: v3.28.0 moves OIDC configuration to its own dedicated admin page (settings_oidc.php) outside the shared settings group form, with per-field save via fetch. The new page won't trigger PM credential-form heuristics. Tracked at #1137.