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.
This commit is contained in:
@@ -38,6 +38,10 @@ describe('OIDC SSO (#798)', () => {
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
process.env.JWT_SECRET = process.env.JWT_SECRET || 'oidc-test-secret';
|
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());
|
({ db, cleanup } = await bootCrmDb());
|
||||||
|
|
||||||
idp = new MockOidcProvider();
|
idp = new MockOidcProvider();
|
||||||
|
|||||||
@@ -390,6 +390,10 @@ router.get('/sso', adminAuth, requirePermission('settings.view'), async (req, re
|
|||||||
try {
|
try {
|
||||||
const oidcService = require('../services/oidcService');
|
const oidcService = require('../services/oidcService');
|
||||||
const cfg = await oidcService.getOidcConfig();
|
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({
|
res.json({
|
||||||
oidc_enabled: cfg.enabled,
|
oidc_enabled: cfg.enabled,
|
||||||
oidc_issuer_url: cfg.issuerUrl || '',
|
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_default_role: cfg.defaultRole,
|
||||||
oidc_button_label: cfg.buttonLabel || '',
|
oidc_button_label: cfg.buttonLabel || '',
|
||||||
oidc_scopes: cfg.scopes,
|
oidc_scopes: cfg.scopes,
|
||||||
redirect_uri: await oidcService.getRedirectUri(),
|
redirect_uri: redirectUri,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to read SSO settings', { error: error.message });
|
logger.error('Failed to read SSO settings', { error: error.message });
|
||||||
|
|||||||
@@ -153,6 +153,14 @@ async function getClient(cfg) {
|
|||||||
async function getRedirectUri() {
|
async function getRedirectUri() {
|
||||||
const { getFrontendBaseUrl } = require('../utils/frontendUrl');
|
const { getFrontendBaseUrl } = require('../utils/frontendUrl');
|
||||||
const base = (await getFrontendBaseUrl()).replace(/\/$/, '');
|
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`;
|
return `${base}/api/auth/admin/sso/callback`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user