From ac1838fbd7352dc55dc3d9bdebca9d1ee57ab866 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Thu, 16 Jul 2026 08:59:48 +0200 Subject: [PATCH] fix(oidc): fail clearly when no public base URL is configured CI exposed that getFrontendBaseUrl() returns '' without FRONTEND_URL or the general_site_url setting (local runs were masked by backend/.env): the flow then sent a RELATIVE redirect_uri to the IdP, which surfaced as an opaque IdP-side error. getRedirectUri now throws OIDC_BAD_CONFIG with an actionable message (login route maps it to sso_error=config); the settings GET degrades to an empty redirect_uri instead of 500ing. The test pins FRONTEND_URL explicitly so it runs identically with and without a local .env. --- backend/__tests__/integration/oidcSso.test.js | 4 ++++ backend/src/routes/adminSettings.js | 6 +++++- backend/src/services/oidcService.js | 8 ++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/__tests__/integration/oidcSso.test.js b/backend/__tests__/integration/oidcSso.test.js index b7f97c87..7524a6b7 100644 --- a/backend/__tests__/integration/oidcSso.test.js +++ b/backend/__tests__/integration/oidcSso.test.js @@ -38,6 +38,10 @@ describe('OIDC SSO (#798)', () => { beforeAll(async () => { process.env.JWT_SECRET = process.env.JWT_SECRET || 'oidc-test-secret'; + // The redirect_uri derives from the public base URL — pin it explicitly: + // CI has no backend/.env, and getFrontendBaseUrl() returning '' makes + // buildAuthorizationRequest fail (by design) with OIDC_BAD_CONFIG. + process.env.FRONTEND_URL = 'http://localhost:5199'; ({ db, cleanup } = await bootCrmDb()); idp = new MockOidcProvider(); diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js index 7f4c60dc..161245c3 100644 --- a/backend/src/routes/adminSettings.js +++ b/backend/src/routes/adminSettings.js @@ -390,6 +390,10 @@ router.get('/sso', adminAuth, requirePermission('settings.view'), async (req, re try { const oidcService = require('../services/oidcService'); const cfg = await oidcService.getOidcConfig(); + // No public base URL configured → surface an empty redirect_uri rather + // than failing the whole settings read; the login route refuses to start + // the flow in that state anyway (OIDC_BAD_CONFIG). + const redirectUri = await oidcService.getRedirectUri().catch(() => ''); res.json({ oidc_enabled: cfg.enabled, oidc_issuer_url: cfg.issuerUrl || '', @@ -399,7 +403,7 @@ router.get('/sso', adminAuth, requirePermission('settings.view'), async (req, re oidc_default_role: cfg.defaultRole, oidc_button_label: cfg.buttonLabel || '', oidc_scopes: cfg.scopes, - redirect_uri: await oidcService.getRedirectUri(), + redirect_uri: redirectUri, }); } catch (error) { logger.error('Failed to read SSO settings', { error: error.message }); diff --git a/backend/src/services/oidcService.js b/backend/src/services/oidcService.js index 4e2887ee..bac4651a 100644 --- a/backend/src/services/oidcService.js +++ b/backend/src/services/oidcService.js @@ -153,6 +153,14 @@ async function getClient(cfg) { async function getRedirectUri() { const { getFrontendBaseUrl } = require('../utils/frontendUrl'); const base = (await getFrontendBaseUrl()).replace(/\/$/, ''); + if (!base) { + // Without a public base URL the redirect_uri would be relative — the IdP + // would reject it with an opaque error on ITS side. Fail here with a + // clear config message instead. + const err = new Error('FRONTEND_URL (or the general_site_url setting) must be set for SSO'); + err.code = 'OIDC_BAD_CONFIG'; + throw err; + } return `${base}/api/auth/admin/sso/callback`; }