Skip to main content
PathMon

Chapter 6 of 15

Setting Up OIDC SSO

Updated Read the full guide

Overview

PatchMon supports OpenID Connect (OIDC) authentication, allowing users to log in via an external Identity Provider (IdP) instead of, or in addition to, local username/password credentials.

Supported Providers

Any OIDC-compliant provider works, including:

  • Authentik
  • Keycloak
  • Okta
  • Azure AD (Entra ID)
  • Google Workspace

What You Get

  • SSO login via a configurable button on the login page
  • Automatic user provisioning on first login (no need to create accounts manually)
  • Group-based role mapping so your IdP controls who is an admin, user, or readonly viewer
  • Optional: disable local password login entirely and enforce SSO for all users

Prerequisites

  • PatchMon already installed and running
  • An OIDC-compatible Identity Provider with an OAuth2/OIDC application configured
  • HTTPS in production (OIDC routes enforce HTTPS when OIDC_ENFORCE_HTTPS=true, which is the default)

Step 1 - Create an OIDC Application in Your IdP

Create a new OAuth2 / OIDC application in your Identity Provider with the following settings:

Setting Value
Application type Web application / Confidential client
Redirect URI https://patchmon.example.com/api/v1/auth/oidc/callback
Scopes openid, email, profile, groups
Grant type Authorization Code
Token endpoint auth Client Secret (Basic)
ID token signing An asymmetric algorithm: RS256 (or RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512, EdDSA)

After creating the application, note the Client ID and Client Secret as you'll need both.

Important: PatchMon cannot accept HS256-signed ID tokens. PatchMon validates ID tokens against the public keys published at your IdP's JWKS endpoint, so the token must be signed asymmetrically with a certificate or key pair. Symmetric HMAC signing (HS256, HS384, HS512), where the client secret doubles as the signing key, is rejected. Most identity providers use RS256 by default, but Authentik does not unless you tell it to. See the Authentik note below.

Tip: If you plan to use group-based role mapping, ensure your IdP includes the groups claim in the ID token. In Authentik, this is enabled by default. In Keycloak, you may need to add a "Group Membership" mapper to the client scope.

Provider-Specific Notes

Authentik:

  • Create an OAuth2/OIDC Provider, then create an Application linked to it
  • Set a Signing Key on the provider. This is required and is the single most common cause of a failed Authentik setup. Open the provider, expand Advanced protocol settings, and set Signing Key to a certificate, for example the built-in authentik Self-signed Certificate. If Signing Key is left empty, Authentik signs ID tokens symmetrically with the client secret using HS256, which PatchMon rejects. Every login then fails with a generic "Authentication failed" message on the login page
  • Replace the default email scope mapping. Authentik's stock mapping always reports the address as unverified, which PatchMon rejects from 2.1.0 onwards. See The verified email requirement
  • Issuer URL format: https://auth.example.com/application/o/patchmon/
  • Groups are included via the groups or ak_groups claim (both are supported)

Keycloak:

  • Create a Client with Access Type confidential
  • Issuer URL format: https://keycloak.example.com/realms/your-realm
  • Add a "Group Membership" protocol mapper to include groups in the token

Okta / Azure AD:

  • Create an OIDC Web Application
  • Ensure groups are included in the ID token claims
  • Entra ID only: it does not send email_verified, which PatchMon requires from 2.1.0 onwards for account linking and auto-creation. Add the xms_edov optional claim instead. See The verified email requirement

Step 2 - Configure PatchMon

Add the following environment variables to your .env file (for Docker deployments) or your server environment.

Required Variables

OIDC_ENABLED=true
OIDC_ISSUER_URL=https://auth.example.com/application/o/patchmon/
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
Variable Description
OIDC_ENABLED Set to true to enable OIDC
OIDC_ISSUER_URL Your IdP's issuer URL (a trailing /.well-known/openid-configuration is removed automatically)
OIDC_CLIENT_ID Client ID from your IdP application
OIDC_CLIENT_SECRET Client secret from your IdP application
OIDC_REDIRECT_URI Must match exactly what you configured in your IdP

Optional Variables

OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=false
OIDC_DEFAULT_ROLE=user
OIDC_DISABLE_LOCAL_AUTH=false
OIDC_BUTTON_TEXT=Login with SSO
OIDC_SESSION_TTL=600
OIDC_POST_LOGOUT_URI=https://patchmon.example.com/login
OIDC_ENFORCE_HTTPS=true
OIDC_SYNC_ROLES=false
Variable Default Description
OIDC_SCOPES openid email profile groups Space-separated scopes to request. Include groups for role mapping
OIDC_AUTO_CREATE_USERS false When true, automatically creates a PatchMon account on first OIDC login. When false, the user must already exist in PatchMon (matched by email)
OIDC_DEFAULT_ROLE user Role assigned when a user doesn't match any group mapping
OIDC_DISABLE_LOCAL_AUTH false When true, hides the username/password fields, only shows the SSO button, and the server refuses password sign-in
OIDC_BUTTON_TEXT Login with SSO Label shown on the SSO login button
OIDC_SESSION_TTL 600 Seconds the OIDC login state is valid. If the user takes longer than this at the IdP, the session expires and they must try again
OIDC_POST_LOGOUT_URI <CORS_ORIGIN>/login Where to redirect after a logout. Defaults to the PatchMon login page
OIDC_ENFORCE_HTTPS true When true, enforces HTTPS on OIDC login and callback routes. Set to false only for local development
OIDC_TRUST_UNVERIFIED_EMAIL false When true, waives the verified-email requirement for account linking and auto-creation. Reduces account-takeover protection
OIDC_SYNC_ROLES false When true, the user's role is updated on every login based on current group membership. When false, roles are managed locally in PatchMon and OIDC login does not change them

Note on APP_ENV: PatchMon reads APP_ENV to determine the runtime environment (e.g. production). NODE_ENV is accepted as a backward-compatibility alias but APP_ENV is preferred.


Step 3 - Group-Based Role Mapping (Optional)

Map your IdP groups to PatchMon roles so that role assignments stay in sync with your directory. Group matching is case-insensitive.

Role Hierarchy

PatchMon checks group membership in this order (highest priority first):

PatchMon Role Required IdP Group(s) Description
Super Admin Member of OIDC_SUPERADMIN_GROUP Full access including managing other superadmins
Admin Member of OIDC_ADMIN_GROUP Full access
Host Manager Member of OIDC_HOST_MANAGER_GROUP Manage hosts and groups
User Member of OIDC_USER_GROUP Standard access with data export
Readonly Member of OIDC_READONLY_GROUP View-only access
Default None of the above Gets OIDC_DEFAULT_ROLE (defaults to user)

Priority: If a user is in multiple groups, the highest-priority role wins. The priority order from highest to lowest is: Super Admin > Admin > Host Manager > Readonly > User.

Environment Variables

OIDC_ADMIN_GROUP=PatchMon Admins
OIDC_USER_GROUP=PatchMon Users
OIDC_SUPERADMIN_GROUP=PatchMon SuperAdmins
OIDC_HOST_MANAGER_GROUP=PatchMon Host Managers
OIDC_READONLY_GROUP=PatchMon Readonly
OIDC_SYNC_ROLES=true
Variable Description
OIDC_ADMIN_GROUP IdP group name that maps to Admin role
OIDC_USER_GROUP IdP group name that maps to User role
OIDC_SUPERADMIN_GROUP IdP group name that maps to Super Admin
OIDC_HOST_MANAGER_GROUP IdP group name that maps to Host Manager role
OIDC_READONLY_GROUP IdP group name that maps to Readonly role
OIDC_SYNC_ROLES When true, the user's role is updated on every login based on current group membership. When false (default), roles are managed locally in PatchMon and OIDC login does not change them

You only need to define the groups you intend to use. Any variables left unset are simply ignored.


Step 4 - Restart PatchMon

After updating your .env file, restart the server so it picks up your OIDC configuration:

# Docker
docker compose restart patchmon-server

# Or if rebuilding
docker compose up -d --force-recreate patchmon-server

# Native systemd installation
sudo systemctl restart <your-domain>

This check needs ENABLE_LOGGING=true, which is the default from 2.0.3. If you have set ENABLE_LOGGING=false, or you are on 2.0.2 or earlier where false was the default, the server writes no logs at all and the commands below return nothing regardless of whether OIDC loaded. Keep LOG_LEVEL at info or lower too, since the confirmation line is logged at info level.

# Docker
docker compose logs patchmon-server | grep -i oidc

# Native systemd
journalctl -u <your-domain> | grep -i oidc

You should see a line confirming SSO is enabled, containing the message OIDC SSO enabled; provider discovery is deferred to the first login attempt along with the issuer and client ID that were loaded.

PatchMon does not contact your identity provider at startup. Provider discovery (the request to .well-known/openid-configuration) is deliberately deferred until the first login attempt, so a temporarily unreachable IdP cannot stop the server from booting. This line therefore confirms only that your four required variables were read and that the SSO button will appear. It does not prove your IdP is reachable or that your provider is configured correctly. The first login attempt is what tests that, and any failure is logged at that point.

The surrounding format depends on APP_ENV. In production, which is the default, logs are JSON:

{"time":"2026-08-12T20:34:34.526Z","level":"INFO","msg":"OIDC SSO enabled; provider discovery is deferred to the first login attempt","version":"2.0.3","port":3000,"issuer":"https://auth.example.com/application/o/patchmon/","client_id":"patchmon","source":"environment variables"}

With APP_ENV set to anything else, the same record is printed as plain text:

time=2026-08-12T20:34:34.526Z level=INFO msg="OIDC SSO enabled; provider discovery is deferred to the first login attempt" version=2.0.3 port=3000 issuer=https://auth.example.com/application/o/patchmon/ client_id=patchmon source="environment variables"

The source field tells you which configuration won: environment variables or database settings. If you edited SSO settings in the web UI but see environment variables, your .env is overriding them.

If you see OIDC is enabled but missing required config: ..., one or more of the four required variables is empty. If you see OIDC is partially configured via env vars but SSO is disabled, you have set some but not all of them.

Note: Releases before 2.0.3 logged nothing at all on a successful OIDC configuration. If you are on 2.0.2 or older, an empty grep -i oidc is expected and does not mean OIDC failed to load. Check whether the SSO button appears on the login page instead.


Step 5 - Test the Login

  1. Open PatchMon in your browser
  2. You should see a "Login with SSO" button (or your custom OIDC_BUTTON_TEXT)
  3. Click it and you'll be redirected to your IdP
  4. Authenticate with your IdP credentials
  5. You'll be redirected back to PatchMon and logged in

If OIDC_AUTO_CREATE_USERS is true, a PatchMon account is created automatically using your email address. The username is taken from the preferred_username claim (e.g. jdoe). If your IdP does not send one, it is derived from the email prefix instead (e.g. john.doe@example.com becomes john.doe). The username is only set when the account is created; renaming it later is done in Settings → Users.


First-Time Setup (No Users Exist Yet)

When PatchMon has no users in the database, it displays a setup wizard. You have two options:

Complete the setup wizard to create your first admin account. This account is created as a Super Admin with full access. You can then enable OIDC from Settings afterwards.

Option B - Log In via OIDC Directly

If you've pre-configured OIDC via environment variables before first boot:

  1. Set OIDC_AUTO_CREATE_USERS=true
  2. The setup wizard is automatically bypassed when OIDC with auto-create is enabled
  3. The first user to log in via OIDC is automatically promoted to Super Admin, regardless of group mapping, to ensure the system always has an admin
  4. Subsequent OIDC users get roles based on group mapping or the default role as normal

Note: You do not need to configure group mapping for the first user. The auto-promotion happens because PatchMon detects no admin exists yet.


What Syncs from Your IdP

On every OIDC login, PatchMon automatically syncs the following from your Identity Provider:

  • Avatar / profile picture: synced if the picture claim is present
  • First name and last name: from given_name and family_name claims
  • Email: used for matching and account linking

The following is only synced when OIDC_SYNC_ROLES=true:

  • Role: based on group membership. When sync is off, roles are managed locally in PatchMon and OIDC login does not change them. You can use OIDC for authentication while still managing roles manually.

Account Linking

If a local PatchMon user already exists with the same email as the OIDC user, PatchMon will automatically link the accounts, but only if the IdP marks that email as verified. See The verified email requirement below, which applies to new accounts too, not only to linking.

The Verified Email Requirement

Applies from PatchMon 2.1.0.

When PatchMon cannot recognise you by a subject it has already stored, the email address is the only thing deciding who you are. Trusting an unverified one would let anyone who can set their own email address at your IdP sign in as an existing PatchMon user. So in that situation PatchMon requires the IdP to state that the address is verified, by sending an email_verified claim set to true.

This applies in two cases:

  • Linking to an existing local account by matching email.
  • Creating a new account on first login when OIDC_AUTO_CREATE_USERS=true.

It does not apply once an account is linked. After a successful first login PatchMon stores the IdP's subject identifier, matches on that from then on, and the email claim stops being the deciding factor.

If the claim is missing, PatchMon treats it as not verified. Several identity providers do not send it by default, and two common ones need configuration:

Authentik

Authentik's stock authentik default OAuth Mapping: OpenID 'email' scope mapping returns email_verified: False unconditionally, because Authentik does not track per-user email verification. Every login therefore fails until you replace that mapping:

  1. Sign in to Authentik as an administrator.

  2. Go to Customisation > Property Mappings.

  3. Select Create, then choose Scope Mapping.

  4. Fill it in as follows:

    • Name: authentik main OAuth Mapping: OpenID verified 'email'

    • Scope name: email

    • Description: Verified Email address

    • Expression:

      return {
          "email": request.user.email,
          "email_verified": True
      }
      
  5. Select Create.

  6. Open the OAuth2 provider you configured for PatchMon and select Edit.

  7. Expand Advanced protocol settings and scroll to Scopes.

  8. Remove authentik default OAuth Mapping: OpenID 'email'.

  9. Add the mapping you created in step 4.

  10. Sign in to PatchMon again.

By making this change you are asserting that the email addresses in your Authentik directory are trustworthy. That is a reasonable statement for a directory you control and populate yourself. It is not reasonable if users can self-register with an arbitrary address.

Microsoft Entra ID

Entra ID does not send email_verified at all, and there is no way to make it. Adding email as an optional claim does not help, because that supplies the address and not the verification signal.

Instead, from PatchMon 2.1.2, add the xms_edov optional claim. This is Microsoft's own "Email Domain Owner Verified" signal, introduced in response to nOAuth, which is the same account-takeover-by-email-spoofing attack this requirement exists to prevent. PatchMon reads it when email_verified is absent, so no security setting has to be relaxed.

  1. In the Microsoft Entra admin centre, open your app registration.
  2. Go to Manage > Token configuration > Add optional claim.
  3. Select token type ID, then add xms_edov. If the portal marks it unrecognised, add it by editing the manifest instead: find the optionalClaims object and add xms_edov to the idToken array.
  4. Sign in to PatchMon again.

On older PatchMon versions, or if you would rather not configure the claim, leave OIDC_AUTO_CREATE_USERS off and link accounts by having each user sign in once while a matching local account exists, or use the opt-in below.

If your provider cannot assert verification at all

Available from PatchMon 2.1.2.

Some directories genuinely have no notion of a verified email address. For those, set Trust unverified email in Settings > OIDC, or OIDC_TRUST_UNVERIFIED_EMAIL=true in the environment. It is off by default.

Set it in the same place you configured OIDC itself. PatchMon takes its whole OIDC configuration from one source or the other, never a mix: if any of OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_SCOPES or OIDC_ENABLED is set in the environment, every OIDC setting comes from the environment and the values in Settings > OIDC are ignored. So if you configured OIDC through environment variables, the toggle in the UI will appear to save but have no effect, and you must set OIDC_TRUST_UNVERIFIED_EMAIL=true instead. If you configured OIDC in the UI, setting only OIDC_TRUST_UNVERIFIED_EMAIL in the environment does nothing, and you must use the toggle. This applies to every OIDC setting, not just this one.

Be clear about what this does. With it on, anyone who can set their own email address at your identity provider can sign in as an existing PatchMon user with that address. It is only reasonable when you control who can change addresses in your directory. Every login it permits is recorded in the server log at warn level, so a relaxed deployment stays visible:

oidc accepting unverified email: trust_unverified_email is enabled

Prefer a real fix where one exists: the Authentik scope mapping above, or xms_edov for Entra.

Other providers

Keycloak, Okta and Google Workspace all send email_verified correctly with their default configuration and need no change.

To check what your IdP actually sends, decode the ID token at jwt.io after a login attempt, or read the rejection in the server log (see Troubleshooting).


Disabling Local Authentication

To enforce SSO for all users, set:

OIDC_DISABLE_LOCAL_AUTH=true

This hides the username/password fields on the login page and only shows the SSO button. The server also refuses password sign-in, the two-factor step and self-service signup, and Discord sign-in is turned off: the Discord button disappears, and a Discord login that was already in progress is refused before any account is created. Local authentication is only actually disabled while OIDC is enabled and fully configured (issuer URL, client ID, client secret and redirect URI all set). This safety check prevents you from being locked out if OIDC is misconfigured. The same setting is the Disable local auth toggle in Settings → OIDC / SSO.

Important: Ensure at least one OIDC user has admin access before enabling this, or you may lose the ability to manage PatchMon.

Existing sessions are not ended. Turning this on blocks new password sign-ins only. Anyone already signed in with a password stays signed in until their session ends: after SESSION_INACTIVITY_TIMEOUT_MINUTES of inactivity, or when the refresh token expires (7 days, or 30 days if they ticked "Remember me"). To force everyone through SSO straight away, deactivate and reactivate the affected users, or change their role and back, which revokes their sessions.

Password Login When the SSO Provider Is Unreachable

With local authentication disabled, an outage at your identity provider means nobody can sign in, because the SSO configuration is still complete. If you want your superadmins to keep a way in during an outage, turn on Superadmin password login if SSO is down in Settings → OIDC / SSO.

  • It is off by default and has no effect unless Disable local auth is on.
  • It is a database setting only, changed on that page. There is no environment variable for it.
  • Only superadmin accounts can use it, and only with two-factor authentication. A superadmin who has not set up two-factor authentication is refused with a message saying so; the attempt counts towards the account lockout and raises a sso_fallback_refused notification event. Every other account is refused with the usual Invalid credentials message, and the attempt counts towards the account lockout.
  • The second factor is always asked for on this path, even on a device that was remembered earlier, and "Remember me" is ignored.
  • Self-service signup and Discord sign-in stay closed.

What counts as unreachable. PatchMon fetches your provider's discovery document (<issuer>/.well-known/openid-configuration) with a 3 second timeout. The provider is treated as unreachable only if that request fails at the network level (DNS failure, connection refused, timeout, TLS error) or the provider answers with an HTTP 5xx error. Any other answer, including a 404 from a mistyped issuer URL, means the provider is up, and password login stays disabled. A provider that rejects a user, wrong credentials and a misconfigured client never count.

One failed check is not treated as an outage: it takes two failed checks in a row, about 5 seconds apart, so a single network blip does not open password login. A healthy result is cached for 30 seconds and an outage for 10, so a burst of sign-in attempts causes at most one request to your provider per window, and repeated logins cannot be used to probe it. After an outage, password login closes again within about 10 seconds of the provider coming back. Saving the OIDC settings clears the cached result. While the provider is unreachable, the login page shows the username and password fields again, with a notice that only superadmins with two-factor authentication can sign in.

Every sign-in completed this way is recorded:

  • a warn line in the server log, with the reason the provider was considered unreachable:

    password login allowed because the SSO provider is unreachable  audit_action=sso_fallback_login username=... reason="discovery request failed: ..."
    
  • a sso_fallback_login notification event, which you can route to an alert channel like any other event (see the admin guide).

  • a row in the audit_logs table with event sso_fallback_login. An account that gives the correct password but is refused by the fallback (not a superadmin, or a superadmin without two-factor authentication) is recorded there too, as a failed sso_fallback_refused row.

Trade-off: anyone who can make your identity provider unreachable from the PatchMon server, for example by disrupting DNS or the network path between them, can bring password login back for superadmin accounts. They would still need a superadmin's password and second factor. Only turn this on if that risk is acceptable for your deployment, and route the sso_fallback_login event to a channel someone watches.

With the setting off, an outage still locks everyone out until the provider recovers. In that case use the database recovery step in Re-enable Local Login When OIDC is Broken.


Complete Example Configuration

Authentik

# .env
APP_ENV=production
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://authentik.example.com/application/o/patchmon/
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret-here
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
OIDC_DEFAULT_ROLE=user
OIDC_BUTTON_TEXT=Login with Authentik
OIDC_ADMIN_GROUP=PatchMon Admins
OIDC_USER_GROUP=PatchMon Users
OIDC_SYNC_ROLES=true

Reminder: On the Authentik side, the provider's Signing Key (under Advanced protocol settings) must be set to a certificate. Leaving it empty makes Authentik sign ID tokens with HS256, which PatchMon rejects, and every login will fail with "Authentication failed".

Keycloak

# .env
APP_ENV=production
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://keycloak.example.com/realms/your-realm
OIDC_CLIENT_ID=patchmon
OIDC_CLIENT_SECRET=your-client-secret-here
OIDC_REDIRECT_URI=https://patchmon.example.com/api/v1/auth/oidc/callback
OIDC_SCOPES=openid email profile groups
OIDC_AUTO_CREATE_USERS=true
OIDC_DEFAULT_ROLE=user
OIDC_BUTTON_TEXT=Login with Keycloak
OIDC_ADMIN_GROUP=PatchMon Admins
OIDC_USER_GROUP=PatchMon Users
OIDC_SYNC_ROLES=true

Troubleshooting

OIDC Not Initialising

Logs show: OIDC is enabled but missing required config: ...

All four required variables must be set: OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI. Check for typos or empty values. A related message, OIDC is partially configured via env vars but SSO is disabled, means some but not all of the four are set.

Login Fails with "no email in UserInfo or id_token"

Logs show: oidc exchange failed with error: oidc: no email in UserInfo or id_token, whilst the login page shows a generic "Authentication failed".

Fixed in 2.1.3. Two separate defects produced the same message. ADFS sends single-valued claims as a JSON array ("email": ["user@example.com"]) in the ID token, and PatchMon only accepted a plain string. Separately, any provider whose UserInfo endpoint returns no email caused that empty value to hide a perfectly good email in the ID token. PatchMon now accepts the array form in the ID token and treats an empty value as missing, so it falls through correctly.

If you are on 2.1.3 or later and still see this, your provider is genuinely not sending an email address. Check that the email scope is requested and that the claim is mapped on the provider side, then decode the ID token at jwt.io after a login attempt to confirm what arrives. PatchMon reads only the standard email claim; there is no claim mapping setting, and upn is not used as a substitute.

A related but distinct failure logs oidc: fetch userinfo: oidc: failed to decode userinfo instead. That means your provider returned an array (or another non-string type) for a field in its UserInfo response rather than in the ID token. That response is decoded before PatchMon sees it, so the array handling above does not apply. Have the provider return plain strings from /userinfo, or map the claim into the ID token instead.

No OIDC Lines in the Startup Logs

On a healthy configuration, PatchMon 2.0.3 and later logs OIDC SSO enabled; provider discovery is deferred to the first login attempt at startup. It does not contact your identity provider at startup, so there are never any discovery or connection messages to look for.

An empty grep -i oidc has four possible causes, and only the last one is a problem with your SSO configuration:

  • Logging is off. ENABLE_LOGGING defaults to true from 2.0.3, but an explicit false in .env or Settings > Environment is still honoured, and it was the default on earlier releases. With logging disabled the server writes nothing at all, so this check tells you nothing
  • LOG_LEVEL is above info. The confirmation line is logged at info level, so warn or error hides it
  • You are on 2.0.2 or older. A correct configuration logged nothing at all on those releases
  • Your configuration did not resolve. Check for missing required config or partially configured in the same output

If logging is off and you would rather not turn it on, check whether the SSO button appears on the login page instead. That is driven by the same resolved configuration.

You may also see a second OIDC line warning that role sync cannot grant superadmin. That is unrelated to whether SSO loaded and is covered under Step 3.

SSO Button Not Appearing

The button appears when all four required variables resolve to non-empty values (from environment variables, or from Settings in the web UI). Because PatchMon does not contact your IdP until someone actually logs in, an unreachable or misconfigured IdP does not hide the button. If the button is missing, the problem is in your configuration values rather than your IdP:

  • One of the four required variables is empty or misspelled. Check the startup logs for missing required config or partially configured
  • OIDC_ENABLED is not true and no OIDC settings have been saved in the web UI
  • The client secret was saved in the web UI but cannot be decrypted, which is logged as OIDC client secret could not be decrypted; treating OIDC as unconfigured. This usually means your encryption key changed. Re-enter and save the secret

"Authentication Failed" After Redirect

This is the generic error for any failure during the token exchange, after your IdP has sent the user back to PatchMon. Check the server logs for the line beginning oidc exchange failed, which contains the specific reason. Common causes:

  • Your IdP is signing ID tokens with HS256. The log contains unexpected signature algorithm "HS256". PatchMon only accepts asymmetrically signed tokens. In Authentik, open the OAuth2/OIDC provider, expand Advanced protocol settings, and set Signing Key to a certificate such as authentik Self-signed Certificate. An empty Signing Key is what causes this
  • The Redirect URI in your IdP does not match OIDC_REDIRECT_URI exactly (including trailing slashes)
  • Cookies are being blocked (OIDC uses httpOnly cookies for session state)
  • Your IdP does not support PKCE (PatchMon uses the S256 code challenge)
  • The log says oidc: UserInfo response has no sub claim or UserInfo sub does not match id_token sub. OpenID Connect requires the IdP's UserInfo endpoint to return the same sub as the ID token. PatchMon has always refused a login where the two differ, and from 2.1.4 it also refuses one where the UserInfo sub is missing. Check the IdP's UserInfo mapping
  • The log says oidc: provider response exceeds size limit. A response from the IdP (discovery document, signing keys, token or UserInfo) was larger than 2 MiB. Usually an oversized groups claim or a proxy returning an error page in place of the IdP. Trim the claims the IdP sends

"Unable to sign in with this account" After a Successful IdP Login

You signed in at your IdP, it sent you back, and PatchMon refused you. The token exchange worked, so this is not the error above. Check the server logs, where one of these lines gives the reason:

Log line Meaning Fix
oidc login rejected: unverified email claim Your IdP sent neither email_verified nor xms_edov. From 2.1.0 PatchMon requires one of these when it has to identify you by email. Affects Authentik and Microsoft Entra ID out of the box The verified email requirement
oidc accepting unverified email: trust_unverified_email is enabled Not an error. Records that a login was allowed through with an unverified address because the opt-in is on Expected if you enabled it deliberately. If not, turn it off in Settings > OIDC
oidc user not found and auto-create disabled No PatchMon account matches, and OIDC_AUTO_CREATE_USERS is off Create the user in PatchMon first, or set OIDC_AUTO_CREATE_USERS=true
oidc login rejected: account is linked to a different issuer The account is linked to the same subject at a different identity provider (issuer). Expected after you point PatchMon at a new IdP Account linked to a different subject
oidc login rejected: account is linked to a different subject The PatchMon account with this email is already bound to another identity at the IdP. The line shows the incoming sub and the stored linked_sub Account linked to a different subject

A deactivated account is a different case and shows "Account disabled" rather than this message, logged as oidc login inactive user.

If your logs show nothing at all, raise LOG_LEVEL to debug and try again.

Account Linked to a Different Subject

The first time someone signs in with SSO, PatchMon stores the IdP's subject identifier (sub) and the issuer it came from against their account, and from then on recognises them by that pair. A subject identifier is only unique within one issuer, so the same sub from a different issuer is treated as a different person. If a later login arrives with the same verified email but a different sub, PatchMon refuses it rather than hand the account to a new identity. That is deliberate: an email address that has been reassigned at the IdP must not inherit someone else's PatchMon account.

The sub changes, and this refusal appears, when:

  • The user was deleted and re-created at the IdP. PocketID, for example, uses its internal user ID as the sub, so a re-created user gets a new one
  • The IdP was reinstalled or restored without its user database
  • The account was first linked through a different IdP, and you have since switched provider. Releases before 2.1.0 let these logins through without checking, so the mismatch may predate your upgrade
  • You pointed PatchMon at a different IdP, or a different realm or application on the same IdP. From 2.1.4 every user already linked through the old issuer is refused until you unlink them, even if the new IdP sends the same sub. This is deliberate: otherwise whoever controls the new IdP could sign in as any linked user, including a superadmin

Accounts linked before 2.1.4 recorded only the IdP's hostname. On each user's next successful login PatchMon checks the hostname against the current issuer and, if it matches, records the full issuer from then on.

Changing your IdP's own URL counts as a new issuer. The issuer is the exact URL your IdP reports, so moving from http to https, changing the port or hostname, or upgrading Keycloak to a version without the /auth prefix refuses every linked user, even though it is the same IdP with the same users. In that case, rebind the links to the new URL rather than unlinking anyone. Find the stored value, then replace it (stock Docker Compose shown):

docker compose exec database psql -U patchmon_user -d patchmon_db -c "SELECT DISTINCT oidc_provider FROM users;"
docker compose exec database psql -U patchmon_user -d patchmon_db \
  -c "UPDATE users SET oidc_provider='<new>' WHERE oidc_provider='<old>';"

Use the issuer your IdP now reports for <new>: the issuer value in its /.well-known/openid-configuration, without a trailing slash. A rebind is safer than an unlink, because each user must still present the same sub as before; an unlinked account is instead claimed by whoever next signs in with its email address. Only rebind when the IdP really is the same one at a new address.

If the new identity is a different IdP, or the user was re-created, and you are sure it is the same person, unlink the old one:

  1. Sign in as an admin, go to Settings > Users and click Edit on the user
  2. Under SSO identity, click Unlink SSO identity and confirm
  3. Ask the user to sign in with SSO again. PatchMon links the account to the new identity, keeping its role and settings

You can only unlink users whose role is strictly below yours (your role holds every permission theirs grants, and at least one more), and never your own account. A superadmin can unlink anyone except themselves. The rule is stricter than for other user changes because, where SSO is the only way to sign in, unlinking an account and changing its email address hands it to whoever owns the new address.

The re-link normally requires the IdP to assert a verified email, as described in The verified email requirement. If you have turned on OIDC_TRUST_UNVERIFIED_EMAIL, that check is skipped: an unlinked account goes to the first identity at your IdP that claims its email address, verified or not. Ask the user to sign in straight after you unlink, and check the server log shows them re-linked. While that setting is on, PatchMon refuses to unlink a superadmin; turn it off first.

If the locked-out account is the only superadmin, and nobody can sign in to reach Settings > Users, clear the link from the database instead. With the stock Docker Compose stack:

docker compose exec database psql -U patchmon_user -d patchmon_db \
  -c "UPDATE users SET oidc_sub = NULL, oidc_provider = NULL WHERE LOWER(email) = LOWER('admin@example.com');"

Replace the address with the account's email, check that it reports UPDATE 1, then sign in with SSO again straight away. If OIDC_TRUST_UNVERIFIED_EMAIL is on, turn it off before running this, for the reason above.

"Failed to reach the OIDC provider" When Clicking the SSO Button

You get this immediately on clicking the SSO button, before your IdP's login page ever appears. The browser shows it as a bare JSON response rather than a styled error page, and the logs show oidc auth url failed.

PatchMon fetches your IdP's discovery document (.well-known/openid-configuration) on the first login attempt rather than at startup, so problems reaching or validating that document surface at this point:

  • PatchMon cannot reach the IdP from inside the container (DNS, firewall, or network policy)
  • OIDC_ISSUER_URL is wrong. Use the issuer URL, not the discovery document URL. If you do paste the full URL ending in /.well-known/openid-configuration, PatchMon removes that suffix before discovery
  • The issuer URL in the discovery document does not match OIDC_ISSUER_URL. For Authentik, the trailing slash matters
  • The IdP's TLS certificate is not trusted by the PatchMon container. The log ends in x509: certificate signed by unknown authority. See the next section

IdP Certificate Signed by an Internal CA

If your identity provider's certificate is issued by a private or internal certificate authority, the PatchMon server does not trust it, and the first login attempt fails with Failed to reach the OIDC provider. The log line reads:

oidc auth url failed ... tls: failed to verify certificate: x509: certificate signed by unknown authority

PatchMon has no option to skip certificate verification for OIDC. Make the server trust your CA instead.

Docker. Mount your CA certificate (PEM format) into the server container and point SSL_CERT_FILE at it:

services:
  server:
    environment:
      SSL_CERT_FILE: /etc/patchmon/internal-ca.pem
    volumes:
      - ./internal-ca.pem:/etc/patchmon/internal-ca.pem:ro

Then recreate the container with docker compose up -d. The server reads the file the first time it opens a TLS connection, so a restart is needed whenever you change it. If you have several CA certificates, either concatenate them into one PEM file or mount them into a directory and set SSL_CERT_DIR to that directory instead. The server runs as a non-root user (UID 65532), so the certificate file must be world-readable (chmod 644 internal-ca.pem). If it is not, PatchMon ignores it and logs the same unknown authority error.

On the stock PatchMon image the system CA bundle in /etc/ssl/certs is still loaded alongside your file, so public certificates (for example your notification destinations) keep working. Do not mount your CA over /etc/ssl/certs/ca-certificates.crt: that replaces the public bundle, and every outbound connection to a publicly signed service will then fail with the same unknown authority error.

Native or LXC installs. Add the CA to the operating system's trust store (on Debian and Ubuntu, copy it to /usr/local/share/ca-certificates/ with a .crt extension and run update-ca-certificates), then restart the PatchMon service.

"Session Expired" Error

The OIDC login state has a configurable window (default 600 seconds via OIDC_SESSION_TTL). If the user takes longer than this at the IdP, the session expires. Simply try logging in again, or increase OIDC_SESSION_TTL if this is happening frequently.

User Gets Wrong Role

  • Check that the groups scope is included in OIDC_SCOPES
  • Verify your IdP is including groups in the ID token (not just the access token)
  • If the logs show oidc no groups in token, your IdP sent no groups at all. Configure it to include the groups claim. In Authentik this means adding a Scope Mapping that emits groups
  • Group matching is case-insensitive, so patchmon admins matches PatchMon Admins

OIDC Banners / Restrictions Appearing When They Shouldn't

If you see "OIDC Authentication Enabled" banners on the Users or Roles settings pages, or the "Add User" / "Add Role" buttons are missing, OIDC_SYNC_ROLES is enabled. These restrictions only apply when role sync is active. If you want to use OIDC for login but manage roles locally, set OIDC_SYNC_ROLES=false (or leave it unset; it defaults to false).

"User Not Found" Error

OIDC_AUTO_CREATE_USERS is false (the default) and no matching PatchMon account exists. Either set OIDC_AUTO_CREATE_USERS=true or create the user account manually in PatchMon first (the email must match).

Debug Logging

For detailed OIDC troubleshooting, enable debug logging:

LOG_LEVEL=debug

Then check the server logs:

# Docker
docker compose logs -f patchmon-server | grep -i oidc

# Native systemd
journalctl -u <your-domain> -f | grep -i oidc

Security Notes

  • HTTPS is enforced for OIDC login and callback routes when OIDC_ENFORCE_HTTPS=true (the default). Set APP_ENV to production in your environment. NODE_ENV is also accepted as a backward-compatibility alias
  • PKCE (S256) is used for all authorization code exchanges
  • Tokens are stored in httpOnly cookies, not localStorage, to prevent XSS attacks
  • Client secrets should never be committed to version control
  • Account linking only occurs when the IdP reports the email as verified
  • Role sync can be disabled (OIDC_SYNC_ROLES=false, which is the default) if you prefer to manage roles manually in PatchMon after first login