Skip to content

feat: implement oidc authentication support for admin user#5495

Closed
xolott wants to merge 2 commits intoNginxProxyManager:developfrom
xolott:feature/oidc-authentication
Closed

feat: implement oidc authentication support for admin user#5495
xolott wants to merge 2 commits intoNginxProxyManager:developfrom
xolott:feature/oidc-authentication

Conversation

@xolott
Copy link
Copy Markdown

@xolott xolott commented Apr 18, 2026

Summary

Adds OpenID Connect (OIDC) Single Sign-On support to Nginx Proxy Manager. Users can authenticate via an external identity provider (Authentik, Keycloak, Authelia, etc.) using the Authorization Code flow. When enabled, a "Sign in with OIDC" button appears on the login page. An optional auto-login mode can skip the local login form entirely and redirect straight to the OIDC provider.

Close #5467 and #5126

image

Changes

  • Added OIDC client library for discovery, authorization, token exchange, user info fetching, and IdP logout
  • Added support for split issuer URLs: a browser-facing URL and an internal backend URL for Docker network setups
  • Added OIDC login, callback, and logout API routes with state/nonce validation via signed JWT cookies
  • Exposed OIDC status (enabled, autoLogin) in the health endpoint
  • Added user matching by configurable claim (default: email) with optional auto-creation on first OIDC login
  • Refactored default admin user creation for reuse by the OIDC auto-create flow
  • Updated OpenAPI schema with OIDC fields
  • Added openid-client dependency
  • Added "Sign in with OIDC" button on the login page
  • Added automatic redirect to OIDC provider when OIDC_AUTO_LOGIN is enabled
  • Extended token storage to track auth method (local / oidc) and optional idTokenHint for logout
  • Fixed Host header forwarding ($host$http_host) in nginx so the original browser port is preserved, fixing OIDC redirects on non-standard ports
  • Added X-Forwarded-Port header to nginx proxy config
  • Added Authentik services and OIDC environment variables to the dev Docker Compose setup
  • Added OIDC documentation section covering all environment variables, Docker network setup, and an Authentik example

Environment Variables

Variable Required Description
OIDC_ISSUER_URL Yes Browser-facing OIDC issuer/discovery URL
OIDC_ISSUER_URL_INTERNAL No Backend-facing issuer URL (for Docker networks). Defaults to OIDC_ISSUER_URL
OIDC_CLIENT_ID Yes OIDC client ID
OIDC_CLIENT_SECRET Yes OIDC client secret
OIDC_REDIRECT_URI Yes Callback URL: http(s)://<host>/api/oidc/callback
OIDC_SCOPES No Scopes (default: openid profile email)
OIDC_IDENTIFIER_FIELD No Claim to match users (default: email)
OIDC_AUTO_CREATE_USER No Auto-create NPM users on first OIDC login
OIDC_AUTO_LOGIN No Auto-redirect to OIDC provider on login page
OIDC_LOGOUT_REDIRECT_URI No Post-logout redirect URL
OIDC_ALLOW_INSECURE_REQUESTS No Allow HTTP issuer URLs (dev only)

Notes

  • Only English and Spanish translations are included for the new UI strings. Other locales will need to be updated separately.
  • OIDC is fully opt-in: no behavior changes unless OIDC_ISSUER_URL and OIDC_CLIENT_ID are set.
  • Tested with Authentik as the identity provider in the dev Docker Compose setup.

Copilot AI review requested due to automatic review settings April 18, 2026 03:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds OpenID Connect (OIDC) SSO support end-to-end (backend routes + frontend login UX) and adjusts proxy/header behavior and documentation to support OIDC redirects in common reverse-proxy/Docker setups.

Changes:

  • Add backend OIDC discovery/auth-code flow endpoints (/api/oidc/login, /api/oidc/callback, /api/oidc/logout) plus user resolution/optional auto-provisioning.
  • Update frontend login page to show “Sign in with OIDC”, optionally auto-redirect, and complete login from the OIDC callback.
  • Expose OIDC status via the health endpoint and adjust nginx proxy headers/docs/dev compose for OIDC testing.

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/src/pages/Login/index.tsx Adds OIDC login button, auto-login redirect, and callback token handling
frontend/src/context/AuthContext.tsx Adds completeOidcLogin() and triggers IdP logout when auth method is OIDC
frontend/src/modules/AuthStore.ts Extends stored token data with authMethod + idTokenHint and supports numeric expiry
frontend/src/api/backend/responseTypes.ts Extends HealthResponse with optional oidc info
frontend/src/locale/src/en.json Adds “Sign in with OIDC” string
frontend/src/locale/src/es.json Adds Spanish translation for “Sign in with OIDC”
backend/routes/oidc.js New OIDC login/callback/logout routes with state/nonce cookie handling
backend/internal/oidc.js Maps OIDC userinfo to existing/new users and issues NPM tokens
backend/lib/oidc.js Wraps openid-client discovery, auth URL building, code exchange, userinfo, logout URL
backend/routes/main.js Adds oidc status (enabled, autoLogin) to health and mounts /oidc routes
backend/schema/components/health-object.json Adds OIDC info to the health schema (currently incomplete vs implementation)
backend/schema/paths/get.json Updates / health endpoint example with OIDC fields
backend/package.json Adds openid-client dependency
backend/yarn.lock Locks openid-client and transitive deps (jose, oauth4webapi)
backend/setup.js Refactors default admin user creation to reusable helper
backend/lib/default-user.js New helper to create the default admin user (reused by OIDC auto-create)
backend/logger.js Adds OIDC logger scope export
docker/rootfs/etc/nginx/conf.d/production.conf Forwards Host as $http_host and adds X-Forwarded-Port for /api
docker/rootfs/etc/nginx/conf.d/dev.conf Same as production for dev, plus websocket location header update
docker/rootfs/etc/nginx/conf.d/include/proxy.conf Adds X-Forwarded-Port forwarding
docker/docker-compose.dev.yml Adds OIDC env vars + Authentik-related config for local testing
docs/src/advanced-config/index.md Adds OIDC configuration documentation section

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/routes/main.js
Comment on lines 36 to +41
res.status(200).send({
status: "OK",
setup,
oidc: {
enabled: oidcEnabled,
autoLogin: oidcEnabled && getOIDCConfig().autoLogin,
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/ health can throw if OIDC_ISSUER_URL + OIDC_CLIENT_ID are set but OIDC_REDIRECT_URI is missing/invalid: oidcEnabled becomes true, then getOIDCConfig() throws while computing autoLogin, causing the entire health check to 500. Consider making isOIDCEnabled() reflect the fully valid config (include OIDC_REDIRECT_URI and other required fields), or wrap getOIDCConfig() here in a try/catch and default autoLogin to false while still reporting enabled: false/misconfigured state.

Suggested change
res.status(200).send({
status: "OK",
setup,
oidc: {
enabled: oidcEnabled,
autoLogin: oidcEnabled && getOIDCConfig().autoLogin,
let oidcHealthEnabled = oidcEnabled;
let oidcAutoLogin = false;
if (oidcEnabled) {
try {
oidcAutoLogin = getOIDCConfig().autoLogin;
} catch {
oidcHealthEnabled = false;
oidcAutoLogin = false;
}
}
res.status(200).send({
status: "OK",
setup,
oidc: {
enabled: oidcHealthEnabled,
autoLogin: oidcAutoLogin,

Copilot uses AI. Check for mistakes.
Comment on lines +187 to +201
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const token = params.get("oidc_token");
const expires = params.get("oidc_expires");
if (!token || !expires) {
return;
}

completeOidcLogin(token, expires, params.get("oidc_id_token") || undefined);
params.delete("oidc_token");
params.delete("oidc_expires");
params.delete("oidc_auth_method");
params.delete("oidc_id_token");
const nextSearch = params.toString();
window.history.replaceState({}, document.title, `${window.location.pathname}${nextSearch ? `?${nextSearch}` : ""}`);
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This flow relies on oidc_token/oidc_expires being present in the URL query string. Even though you call replaceState, the sensitive token can still be exposed via browser history, logging, or Referer headers before React mounts. If possible, switch the backend callback to deliver tokens without putting them in the URL (e.g., HttpOnly cookie or one-time code exchange).

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +52
OIDC_CLIENT_ID: "7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU"
OIDC_CLIENT_SECRET: "VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq"
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dev compose file hard-codes an OIDC client secret (and client id). Even for development, committing real-looking secrets is risky and encourages copy/paste into production. Replace these with clearly fake placeholder values (or load them from a local .env file that is gitignored).

Suggested change
OIDC_CLIENT_ID: "7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU"
OIDC_CLIENT_SECRET: "VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq"
OIDC_CLIENT_ID: "dev-placeholder-oidc-client-id"
OIDC_CLIENT_SECRET: "dev-placeholder-oidc-client-secret"

Copilot uses AI. Check for mistakes.
"enabled": {
"type": "boolean",
"description": "Whether OIDC authentication is configured",
"example": false
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /api/ health response now includes oidc.autoLogin, but the OpenAPI schema for health-object only allows oidc.enabled and sets additionalProperties: false. This makes the live response fail schema validation (see test/cypress/e2e/api/Health.cy.js, which validates /). Add autoLogin to the schema (and update required if appropriate).

Suggested change
"example": false
"example": false
},
"autoLogin": {
"type": "boolean",
"description": "Whether OIDC automatic login is enabled",
"example": false

Copilot uses AI. Check for mistakes.
"status": "OK",
"setup": true,
"oidc": {
"enabled": false
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The health endpoint example under / was updated to include oidc.enabled but omits oidc.autoLogin, while the implementation returns both fields. Update the example to match the actual response shape so the published docs stay accurate.

Suggested change
"enabled": false
"enabled": false,
"autoLogin": false

Copilot uses AI. Check for mistakes.
Comment thread backend/routes/oidc.js
Comment on lines +165 to +174
const origin = getRequestOrigin(req);
const redirectUrl = new URL(stateData.redirectPath || "/", origin);
redirectUrl.searchParams.set("oidc_token", auth.token);
redirectUrl.searchParams.set("oidc_expires", auth.expires);
redirectUrl.searchParams.set("oidc_auth_method", "oidc");
if (tokenSet.id_token) {
redirectUrl.searchParams.set("oidc_id_token", tokenSet.id_token);
}

res.redirect(302, redirectUrl.toString());
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid redirecting back to the SPA with the API token in the query string (oidc_token, oidc_expires, oidc_id_token). Query parameters are commonly captured in browser history, reverse-proxy access logs, and can leak via the Referer header to other same-origin requests made before the SPA calls replaceState. Prefer delivering the token via an HttpOnly cookie, or redirect with a short-lived one-time code that the SPA exchanges server-side for a token.

Copilot uses AI. Check for mistakes.
Comment thread backend/lib/oidc.js
options.execute = [allowInsecureRequests];
}

const discoveryUrl = getDiscoveryUrl( config.issuerUrlInternal).toString();
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor formatting: there’s an extra space in getDiscoveryUrl( config.issuerUrlInternal) which will fail many linters/formatters. Please run the formatter or remove the stray space.

Suggested change
const discoveryUrl = getDiscoveryUrl( config.issuerUrlInternal).toString();
const discoveryUrl = getDiscoveryUrl(config.issuerUrlInternal).toString();

Copilot uses AI. Check for mistakes.
Comment thread backend/internal/oidc.js
Comment on lines +93 to +106
const getIdentityFromUserInfo = (userinfo) => {
const config = getOIDCConfig();
const identifier = userinfo?.[config.identifierField] || userinfo?.email;

if (!identifier || typeof identifier !== "string") {
throw new errs.AuthError("OIDC profile is missing the configured identifier");
}

return identifier.toLowerCase().trim();
};

const resolveUser = async (userinfo) => {
const email = getIdentityFromUserInfo(userinfo);
let user = await findUserByEmail(email);
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OIDC_IDENTIFIER_FIELD is treated as the value to look up (and potentially create) the user’s email, but it can be configured to any claim. If a non-email claim is chosen (e.g. preferred_username), this will store a non-email string in the email column and break assumptions elsewhere (UI validation, uniqueness, notifications, etc.). Consider either (1) validating that the resolved identifier is a valid email address and erroring otherwise, or (2) renaming/limiting the config so it’s clearly an email claim (e.g. OIDC_EMAIL_CLAIM).

Copilot uses AI. Check for mistakes.
}, [completeOidcLogin]);

useEffect(() => {
console.log(health.data)
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the leftover console.log(health.data) debug statement; it will spam the browser console for all users hitting the login page (and can leak configuration details).

Suggested change
console.log(health.data)

Copilot uses AI. Check for mistakes.
@StuFrankish
Copy link
Copy Markdown

@xolott - I had a think about this over the weekend and decided to extend my branch to cover this use-case.
Would you mind having a look to see if PR #5494 (specifically my additional comment here) now covers your needs as well?

It would be beneficial to have a solution that supports as many use-cases as possible.

@xolott
Copy link
Copy Markdown
Author

xolott commented Apr 21, 2026

That covers my use case. I think I can close my PR now. I hope it gets merged.

Thanks @StuFrankish

@xolott xolott closed this Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OIDC Support

3 participants