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 through ipam_setting('oidc.*'). Configure everything from ⚙ Admin → Settings → OIDC / SSO — edits take effect on the next login with no config.php change and no restart. The config.php fallback 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, and oidc.hide_emergency_link are now registered database settings you can toggle from the admin UI.

Contents


How it works

  1. User clicks Sign in with <display_name> on the login page
  2. Browser is redirected to the IdP with a PKCE code challenge, state, and nonce
  3. User authenticates at the IdP
  4. IdP redirects back to oidc_callback.php with an authorization code
  5. The callback exchanges the code for an ID token (verifying the PKCE code verifier)
  6. The ID token signature is verified against the IdP's JWKS (RS256/RS384/RS512)
  7. The sub claim is matched to a local user account; if auto_provision is enabled a new account is created if none exists
  8. 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 openssl extension (standard on most hosts)
  • HTTPS — OIDC callbacks must be served over HTTPS
  • allow_url_fopen = On in php.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

KeyRequiredDescription
enabledyesSet to true to activate OIDC
display_namenoButton label on the login page (default: SSO)
client_idyesOAuth 2.0 client ID from your IdP
client_secretyesOAuth 2.0 client secret from your IdP
discovery_urlyesIdP base URL — /.well-known/openid-configuration is appended automatically, or supply the full path
redirect_uriyesMust match exactly what is registered with the IdP
scopesnoSpace-separated scopes (default: openid email profile)
auto_linknoLink an incoming OIDC login to an existing unlinked local account by preferred_username then email (default: false). Implied by auto_provision.
auto_provisionnoCreate a local user on first OIDC login if none exists (default: false). Enabling this also flips auto_link on.
default_rolenoRole 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_loginnoHide the password form when OIDC is enabled (default: false). See Disabling local login
hide_emergency_linknoHide 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_bypassnoMake ?local=1 completely non-functional (default: false). Database-backed since v2.7.0.

IdP setup examples

Google

  1. Go to Google Cloud Console → APIs & Services → Credentials
  2. Create an OAuth 2.0 Client ID (type: Web application)
  3. Add https://ipam.example.com/oidc_callback.php to Authorized redirect URIs
  4. Copy the client ID and secret
'discovery_url' => 'https://accounts.google.com',

Microsoft Entra ID (Azure AD)

  1. Azure Portal → App registrations → New registration
  2. Set redirect URI to https://ipam.example.com/oidc_callback.php (platform: Web)
  3. Under Certificates & secrets, create a new client secret
  4. Note your tenant ID
'discovery_url' => 'https://login.microsoftonline.com/{tenant-id}/v2.0',

Okta

  1. Okta Admin Console → Applications → Create App Integration (OIDC, Web Application)
  2. Add https://ipam.example.com/oidc_callback.php to Sign-in redirect URIs
  3. Copy the client ID and secret
'discovery_url' => 'https://your-org.okta.com/oauth2/default',

Keycloak

  1. Keycloak Admin → Realm → Clients → Create
  2. Set Valid Redirect URIs to https://ipam.example.com/oidc_callback.php
  3. Enable Client authentication (confidential client)
'discovery_url' => 'https://keycloak.example.com/realms/your-realm',

Authentik

  1. Authentik Admin → Applications → Providers → Create OAuth2/OpenID Connect Provider
  2. 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

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:

  1. Create or locate the local user account in Admin → Users
  2. In the user's Actions panel, paste the IdP sub claim value into the Link SSO field and submit
  3. 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:

  1. On the first OIDC login, the sub claim is looked up in users.oidc_sub — no match found
  2. An existing unlinked account is sought: first by matching preferred_username, then by matching email (against both the username and email columns)
  3. If a match is found, the account is automatically linked to the sub claim and name/email are populated from the ID token if they were blank
  4. If no match is found, a new account is created:
    • Username derived from preferred_username claim → email local-part → sub (fallback)
    • Name and email populated from the name and email claims
    • Unusable random password (account cannot be used for local auth)
    • Role set to default_role (default: readonly)

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.

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_bypass is true, 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.

SymptomLikely cause
"Could not reach the identity provider"discovery_url is wrong or unreachable; check allow_url_fopen
"SSO authentication failed" after redirectCheck the PHP error log for the specific reason
"state mismatch" in error logSession 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 expiredLarge clock skew between your server and the IdP — sync server time via NTP
Redirect URI mismatch error from IdPredirect_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.