Skip to content

Bug fixes & generic OIDC/SSO provider support#5494

Open
StuFrankish wants to merge 1 commit intoNginxProxyManager:developfrom
StuFrankish:develop
Open

Bug fixes & generic OIDC/SSO provider support#5494
StuFrankish wants to merge 1 commit intoNginxProxyManager:developfrom
StuFrankish:develop

Conversation

@StuFrankish
Copy link
Copy Markdown

@StuFrankish StuFrankish commented Apr 17, 2026

Summary

This PR contains three changes: a bug fix for Nginx config handling, a full OIDC/SSO feature implementation and a frontend email validation fix.

Closes #3678
Closes #5126
Closes #5370
Closes #5467

1. Bug Fix: Nginx config detection and error preservation (#3678)

Problem:
The advancedConfigHasDefaultLocation function in backend/internal/nginx.js had two bugs:

  • Comment lines interfered with location block detection, causing false positives/negatives
  • The regex didn't handle Nginx location modifiers (=, ~, ~*, ^~)

Additionally, renameConfigAsError was deleting the config file instead of preserving it as .err for debugging and a redundant deleteConfig call followed immediately after.

Ultimately resulting in a failure and users seeing the OpenResty welcome page.

Changes:

  • backend/internal/nginx.js
    • Strip comment lines before matching
    • enhanced regex to support all Nginx location modifiers and better whitespace handling
    • fixed renameConfigAsError to delete the old .err file first then rename (preserving the config for debugging)
    • removed redundant deleteConfig call in the error path

2. Bug Fix: Email validation inconsistency (#5370)

Problem:
The frontend email validation regex in Validations.tsx was more permissive than the backend schema pattern in common.json. The frontend accepted emails without a TLD (e.g., user@localhost, user@internalserver) while the backend required a dot followed by a 2+ letter TLD. This meant users could complete registration/setup with an email the backend would later reject, potentially locking them out.

Pattern Location Requires TLD?
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+$/i Frontend (old) No
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ Backend schema Yes

Changes:

  • frontend/src/modules/Validations.tsx
    • Updated email regex to /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, matching the backend pattern and ensuring consistent validation across all forms (setup, login, user management)

3. Feature: OpenID Connect (OIDC/SSO) provider support (#5467, #5126)

Note

I missed a valid use-case driver, so I've amended this branch and documented it in this comment below.

Feature Request:
Nginx Proxy Manager only supports local email/password authentication. Users leveraging centralized identity providers have no way to integrate SSO, forcing separate credential management.

Features Added:

  • Authorization code flow with PKCE (S256), nonce validation and state CSRF protection
  • Auto-discovery of OIDC provider configuration with caching
  • Auto-provisioning of new users on first OIDC login
  • Account linking between existing local accounts and OIDC identities
  • Pushed Authorization Requests (PAR, RFC 9126) as a per-provider toggle
  • AES-256-GCM encryption of client secrets at rest
  • Integrated test connection workflow validating discovery, credentials (via PAR) and scopes
  • Settings UI with table + modal pattern for provider CRUD (matching existing Users page)
  • OIDC login buttons on the login page
  • OIDC-only users blocked from 2FA enrollment (IdP manages security policies)
  • Admin-only access gates on all configuration and test-connection endpoints

Changes:

Backend

  • backend/internal/oidc.js - Core OIDC module: discovery with caching, PKCE-enforced auth code flow, callback handling, user resolution/auto-provisioning, account linking, test connection with PAR-based validation (credentials + scopes), fallback to scopes_supported metadata
  • backend/routes/oidc.js - API routes for providers, config CRUD, authorize, callback (HTML response with XSS-safe error whitelisting), logout, link, test-connection
  • backend/lib/crypto.js - AES-256-GCM encryption for client secrets at rest, key derived via HKDF from RSA private key with purpose-bound label
  • backend/internal/user.js - hasPasswordAuth flag on the user model, clear error messages for OIDC-only users attempting password operations
  • backend/internal/2fa.js - OIDC-only users are blocked from 2FA enrollment (requirePasswordAuth guard) - assumption is their IdP manages security policies
  • backend/migrations/20260409000000_oidc_support.js - Alter setting.value to TEXT (for encrypted secrets in JSON), add composite index on auth table for fast OIDC sub lookup
  • backend/schema/paths/oidc/ - Full OpenAPI schema for all OIDC endpoints with additionalProperties: false, HTTPS pattern enforcement, auto_provision_role enum restricted to "user"

Frontend

  • frontend/src/pages/Settings/OidcSettings.tsx - Settings page orchestrator with provider CRUD, dark-mode-safe table wrapper
  • frontend/src/pages/Settings/OidcProviderTable.tsx - Provider table using @tanstack/react-table with name, provider ID, status badge, and actions dropdown
  • frontend/src/modals/OidcProviderModal.tsx - Tabbed modal (Connection / Options / Claim Mapping) with auto-ID generation, PAR toggle, callback URL with copy button, rich test connection results (discovery pass, credentials pass/warn/fail, scopes pass/fail)
  • frontend/src/pages/Login/index.tsx - OIDC login buttons on the login page
  • frontend/src/api/backend/ - API layer: getOidcConfig, getOidcProviders, updateOidcConfig, testOidcConnection with typed TestOidcConnectionResult
  • frontend/src/hooks/ - useOidcConfig and useOidcProviders React Query hooks
  • frontend/src/context/AuthContext.tsx - OIDC callback token handling
  • frontend/src/components/SiteHeader.tsx - External auth indicator
  • frontend/src/pages/Users/Table.tsx - External auth icon on user table rows

Infrastructure

  • docker/rootfs/etc/nginx/conf.d/ - X-Forwarded-Port header in dev and production nginx configs
  • docker/docker-compose.dev.windows.yml - Windows dev compose override with bind mounts

Security:

A comprehensive AI-assisted security audit was performed. Key findings:

Implemented correctly:

  • PKCE S256 with code_verifier/code_challenge - prevents authorization code interception
  • Nonce in state JWT, verified at token exchange - prevents replay attacks
  • State parameter as signed 5-minute JWT with PKCE + nonce + provider ID - CSRF protection
  • AES-256-GCM with HKDF-derived key for client secret encryption at rest
  • Secrets redacted (********) in all API responses - never sent to frontend
  • HTTPS enforcement on all discovery URLs - SSRF mitigation
  • No email-based auto-linking - prevents account takeover via email collision
  • Auto-provisioned users always role: "user" (schema-enforced enum) - no privilege escalation
  • RS256 with pinned algorithm - prevents alg=none attacks
  • XSS-safe callback HTML - error code whitelist, htmlEncode(), JSON.stringify()
  • Admin-only gates on all config and test-connection endpoints
  • Schema validation with additionalProperties: false on all endpoints
  • OIDC-only users blocked from 2FA enrollment - their IdP manages security policies

Known non-blocking findings (defense-in-depth, not exploitable):

  • State JWT replay window (mitigated by single-use auth codes + PKCE)
  • Token storage in localStorage (pre-existing app-wide pattern)
  • Discovery cache keyed by URL only, not client_id (mitigated by cache-bust on save/test)

Testing:

Automated Tests (backend/test/oidc.test.js):

  • Crypto: AES-256-GCM encrypt/decrypt round-trip, random IV uniqueness, tampered ciphertext rejection, wrong key rejection, HKDF consistency and purpose-label isolation
  • XSS prevention: HTML encoding of all special characters (<>&"'), XSS payload neutralization
  • Error whitelisting: All 7 OIDC error codes map to safe messages, unknown/XSS codes return generic message, user-supplied content never reflected
  • HTTPS enforcement: Accepts HTTPS, rejects HTTP/file/ftp/null/embedded-HTTPS URLs
  • Role safety: Schema enum confirms auto_provision_role only allows "user"
  • State scope validation: Accepts oidc-state, rejects missing/wrong/empty scopes

Manual Testing:

  • Configure providers (Authentik, Keycloak, Azure AD) via Settings -> Authentication
  • Test connection validates discovery document, credentials (via PAR), and scopes
  • Login via OIDC button, verify auto-provisioning creates user with standard role
  • Account linking via user profile
  • Verify OIDC-only users cannot access 2FA enrollment (clear error message)
  • Dark mode: verify table wrapper, buttons, badges, modal all render correctly
  • Empty state: "No providers configured" message with guidance

Screenshots

Login Screen Screenshot_17-4-2026_213244_127 0 0 1
Profile & User Management Screenshot_17-4-2026_212957_127 0 0 1 Screenshot_17-4-2026_213023_127 0 0 1 Screenshot_17-4-2026_213039_127 0 0 1
Settings Screenshot_17-4-2026_21313_127 0 0 1 Screenshot_17-4-2026_21325_127 0 0 1 Screenshot_17-4-2026_213119_127 0 0 1

@StuFrankish StuFrankish force-pushed the develop branch 3 times, most recently from a9fb665 to ad3243f Compare April 17, 2026 21:29
@StuFrankish StuFrankish marked this pull request as ready for review April 17, 2026 21:45
@StuFrankish
Copy link
Copy Markdown
Author

So I missed a valid use-case - configuring NPM through environment vars for automated deployment scenarios - for the folks with more advanced homelab setups.
I've now included a way to configure NPM directly with environment vars, or by pointing it at a config file (cleaner imho).

Here are the the changes;

Config File

The container automatically looks for /data/oidc-providers.json on startup. To use a different path, set the OIDC_CONFIG_FILE environment variable.

Sample config file

{
  "providers": [
    {
      "id": "authentik",
      "name": "Authentik",
      "discovery_url": "https://auth.example.com/application/o/npm/.well-known/openid-configuration",
      "client_id": "npm-client",
      "client_secret": "${OIDC_AUTHENTIK_SECRET}",
      "scopes": "openid email profile",
      "enabled": true,
      "auto_provision": true,
      "use_par": false,
      "claim_mapping": {
        "email": "email",
        "name": "name",
        "nickname": "preferred_username",
        "avatar": "picture"
      }
    }
  ]
}

Provider fields

These mirror the UI, so nothing new here.

Field Type Required Default Description
id string yes - Unique identifier (slug) for the provider
name string yes - Display name shown on the login button
discovery_url string yes - OpenID Connect discovery endpoint. Must use HTTPS.
client_id string yes - Client ID from your identity provider
client_secret string no "" Client secret. Supports ${OIDC_*} placeholders.
scopes string no openid email profile Space-separated OIDC scopes
enabled boolean no true Whether this provider is active
auto_provision boolean no false Auto-create user accounts on first login
use_par boolean no false Use Pushed Authorization Requests
claim_mapping object no (see above) Maps token claims to user profile fields

Secret Handling

While folks can do what they like, secrets ought to be well, secret... so best practice advice is to use placeholders in the config file (Use ${OIDC_*})

Placeholders are resolved from environment variables at startup. Only OIDC_-prefixed variables are expanded - this prevents accidental exposure of database passwords or other sensitive values in the container environment.

"client_secret": "${OIDC_AUTHENTIK_SECRET}"

Note: File-sourced secrets are held in memory only and are never written to the database or encrypted by the application - they rely on OS-level file permissions and container isolation for protection.

Docker Compose Examples

Default path (recommended)

services:
  npm:
    image: nginx-proxy-manager
    volumes:
      - npm-data:/data
      - ./oidc-providers.json:/data/oidc-providers.json:ro
    environment:
      OIDC_AUTHENTIK_SECRET: "your-client-secret"
    ports:
      - "80:80"
      - "443:443"
      - "81:81"

volumes:
  npm-data:

Custom path

services:
  npm:
    image: nginx-proxy-manager
    volumes:
      - npm-data:/data
      - ./config:/config:ro
    environment:
      OIDC_CONFIG_FILE: "/config/oidc-providers.json"
      OIDC_AUTHENTIK_SECRET: "your-client-secret"
    ports:
      - "80:80"
      - "443:443"
      - "81:81"

volumes:
  npm-data:

Environment variables only (single provider)

For a single provider, no config file is needed:

services:
  npm:
    image: nginx-proxy-manager
    volumes:
      - npm-data:/data
    environment:
      OIDC_PROVIDER_ID: "authentik"
      OIDC_PROVIDER_NAME: "Authentik"
      OIDC_PROVIDER_DISCOVERY_URL: "https://auth.example.com/application/o/npm/.well-known/openid-configuration"
      OIDC_PROVIDER_CLIENT_ID: "npm-client"
      OIDC_PROVIDER_CLIENT_SECRET: "your-client-secret"
      OIDC_PROVIDER_AUTO_PROVISION: "true"
    ports:
      - "80:80"
      - "443:443"
      - "81:81"

volumes:
  npm-data:

Multiple providers

{
  "providers": [
    {
      "id": "authentik",
      "name": "Authentik",
      "discovery_url": "https://auth.example.com/application/o/npm/.well-known/openid-configuration",
      "client_id": "npm-client",
      "client_secret": "${OIDC_AUTHENTIK_SECRET}"
    },
    {
      "id": "keycloak",
      "name": "Keycloak",
      "discovery_url": "https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration",
      "client_id": "npm-client",
      "client_secret": "${OIDC_KEYCLOAK_SECRET}"
    }
  ]
}

Environment Variable Reference

Variable Description Default
OIDC_CONFIG_FILE Path to the JSON config file /data/oidc-providers.json
OIDC_PROVIDER_ID Provider ID (slug) (not set)
OIDC_PROVIDER_NAME Display name Same as ID
OIDC_PROVIDER_DISCOVERY_URL Discovery URL (must be HTTPS) (required)
OIDC_PROVIDER_CLIENT_ID Client ID (required)
OIDC_PROVIDER_CLIENT_SECRET Client secret (plaintext) (empty)
OIDC_PROVIDER_SCOPES Requested scopes openid email profile
OIDC_PROVIDER_ENABLED Enable this provider true
OIDC_PROVIDER_AUTO_PROVISION Auto-create users on login false
OIDC_PROVIDER_USE_PAR Enable PAR flow false
OIDC_PROVIDER_CLAIM_EMAIL Claim name for email email
OIDC_PROVIDER_CLAIM_NAME Claim name for display name name
OIDC_PROVIDER_CLAIM_NICKNAME Claim name for username preferred_username
OIDC_PROVIDER_CLAIM_AVATAR Claim name for avatar URL picture

Conflict Resolution

If the same provider ID is defined in multiple sources, priority is:

  1. Config file - highest
  2. Environment variables - used if ID not already in file
  3. Database (UI) - lowest; file-sourced provider with the same ID takes over

A warning is logged when a conflict is detected.

Security Notes

  • auto_provision_role is always forced to "user" regardless of config file value - admin access cannot be granted via file config.
  • Environment variable expansion is restricted to OIDC_-prefixed variables to prevent leaking other secrets.
  • HTTPS is enforced for all discovery_url values. HTTP URLs are rejected at startup.

@alexsalex
Copy link
Copy Markdown

alexsalex commented Apr 21, 2026

We're waiting for OIDC implementation already for 3 years. NOBODY CARE ABOUT IT. And your PR will be ignored.

So sad...

BTW:

I got this with Authentik when I try to edit the provider in NPM:

data/providers/0 must NOT have additional properties

And can't save changes.

In the NPM logs I got:

unknown format "uri" ignored in schema at path "#/properties/discovery_url"
unknown format "uri" ignored in schema at path "#/properties/discovery_url"

When I try to link the account, Authentik shows this:

Redirect URI Error

The request fails due to a missing, invalid, or mismatching redirection URI (redirect_uri).

But I able to login with Authentik.

@StuFrankish
Copy link
Copy Markdown
Author

Thanks for you feedback @alexsalex

We're waiting for OIDC implementation already for 3 years. NOBODY CARE ABOUT IT. And your PR will be ignored.
So sad...

That's up to @jc21 - there are no other maintainers so not much I can do but make the offer.

I got this with Authentik when I try to edit the provider in NPM:
data/providers/0 must NOT have additional properties

And can't save changes.
In the NPM logs I got:

unknown format "uri" ignored in schema at path "#/properties/discovery_url"
unknown format "uri" ignored in schema at path "#/properties/discovery_url"

Thanks for letting me know, I've found two small bugs and will address them both later tonight.
unknown format "uri" ignored in schema is just an internal warning and can be ignored (I'll still deal with it though).

When I try to link the account, Authentik shows this:

Redirect URI Error
The request fails due to a missing, invalid, or mismatching redirection URI (redirect_uri).

But I able to login with Authentik.

Have you added both callback URI's to the config in Authentik?
Linking an account uses a different callback path by design.

Paths are;

https://<npm-host>/api/oidc/callback
https://<npm-host>/api/oidc/link-callback

I'll also add this to the "External Authentication" Settings page to make it clearer. 👍

…dmin UI

Implements a complete OpenID Connect integration allowing users to sign in
via external identity providers (Authentik, Keycloak, Authelia, Google, etc).

- OIDC provider management with discovery, PKCE (S256), and encrypted client secrets
- Pushed Authorization Requests (PAR) support per RFC 9126
- Account linking/unlinking with lockout prevention and 2FA compatibility
- Admin settings UI with provider table, modal editor, and test connection
- Auto-provisioning of new users with restricted default roles
- Audit logging with per-provider change tracking
- Help documentation and full i18n coverage
@nginxproxymanagerci
Copy link
Copy Markdown

Docker Image for build 6 is available on DockerHub:

nginxproxymanager/nginx-proxy-manager-dev:pr-5494

Note

Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
This is a different docker image namespace than the official image.

Warning

Changes and additions to DNS Providers require verification by at least 2 members of the community!

@alexsalex
Copy link
Copy Markdown

alexsalex commented Apr 21, 2026

@StuFrankish

That's up to @jc21 - there are no other maintainers so not much I can do but make the offer.

Don't give up, please....

Have you added both callback URI's to the config in Authentik?
Linking an account uses a different callback path by design.

No.. added.

After I repull the image and add second callback URL everything start working for me as expected. I was able to link the account and edit the SSO provider in settings.

@alexsalex
Copy link
Copy Markdown

alexsalex commented Apr 21, 2026

Only this still appears, but it's Warning:

Provider does not support PAR — credentials will be validated during login

Use Authentik.

Its not affect the SSO work, just FYI.

@StuFrankish
Copy link
Copy Markdown
Author

Only this still appears, but it's Warning:

Provider does not support PAR — credentials will be validated during login

Use Authentik.
Its not affect the SSO work, just FYI.

I'm not super familiar with Authentik, but what I could find suggests that while it is supported, PAR must be explicitly enabled on the OAuth2/OIDC provider in Authentik's admin UI - it's not on by default.

Without PAR, I can't pre-validate credentials server-side during the "Test Connection" step, but login will still work fine.
I'll make a note of that for future 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants