Route CMS OIDC calls through internal proxy

This commit is contained in:
DCCONSTRUCTIONS 2026-07-06 09:42:03 +03:00
parent 88ad0557ad
commit a836d3639b
4 changed files with 76 additions and 9 deletions

View File

@ -39,6 +39,9 @@ CMS_BOOTSTRAP_ADMIN_NAME=DCTOUCH CMS Admin
CMS_AUTH_ENABLED=true CMS_AUTH_ENABLED=true
CMS_BASE_URL=http://cms.local.nodedc:8210 CMS_BASE_URL=http://cms.local.nodedc:8210
CMS_OIDC_ISSUER=http://cms-auth.local.nodedc:8210/application/o/dc-cms/ CMS_OIDC_ISSUER=http://cms-auth.local.nodedc:8210/application/o/dc-cms/
CMS_OIDC_INTERNAL_BASE_URL=http://reverse-proxy
CMS_OIDC_INTERNAL_HOST=cms-auth.local.nodedc
CMS_OIDC_INTERNAL_PROTO=http
CMS_OIDC_CLIENT_ID=dc-cms CMS_OIDC_CLIENT_ID=dc-cms
CMS_OIDC_CLIENT_SECRET=replace-with-random-secret CMS_OIDC_CLIENT_SECRET=replace-with-random-secret
CMS_OIDC_REDIRECT_URI=http://cms.local.nodedc:8210/auth/callback CMS_OIDC_REDIRECT_URI=http://cms.local.nodedc:8210/auth/callback

View File

@ -40,6 +40,9 @@ CMS_BOOTSTRAP_ADMIN_NAME=DCTOUCH CMS Admin
CMS_AUTH_ENABLED=true CMS_AUTH_ENABLED=true
CMS_BASE_URL=https://cms.dcserve.ru CMS_BASE_URL=https://cms.dcserve.ru
CMS_OIDC_ISSUER=https://auth.dcserve.ru/application/o/dc-cms/ CMS_OIDC_ISSUER=https://auth.dcserve.ru/application/o/dc-cms/
CMS_OIDC_INTERNAL_BASE_URL=http://reverse-proxy
CMS_OIDC_INTERNAL_HOST=auth.dcserve.ru
CMS_OIDC_INTERNAL_PROTO=https
CMS_OIDC_CLIENT_ID=dc-cms CMS_OIDC_CLIENT_ID=dc-cms
CMS_OIDC_CLIENT_SECRET=replace-with-random-secret CMS_OIDC_CLIENT_SECRET=replace-with-random-secret
CMS_OIDC_REDIRECT_URI=https://cms.dcserve.ru/auth/callback CMS_OIDC_REDIRECT_URI=https://cms.dcserve.ru/auth/callback

View File

@ -38,6 +38,9 @@ services:
SITE_ROOT: /sites/nodedc SITE_ROOT: /sites/nodedc
CMS_AUTH_ENABLED: ${CMS_AUTH_ENABLED:-true} CMS_AUTH_ENABLED: ${CMS_AUTH_ENABLED:-true}
CMS_OIDC_ISSUER: ${CMS_OIDC_ISSUER:?cms oidc issuer required} CMS_OIDC_ISSUER: ${CMS_OIDC_ISSUER:?cms oidc issuer required}
CMS_OIDC_INTERNAL_BASE_URL: ${CMS_OIDC_INTERNAL_BASE_URL:-}
CMS_OIDC_INTERNAL_HOST: ${CMS_OIDC_INTERNAL_HOST:-}
CMS_OIDC_INTERNAL_PROTO: ${CMS_OIDC_INTERNAL_PROTO:-}
CMS_OIDC_CLIENT_ID: ${CMS_OIDC_CLIENT_ID:?cms oidc client id required} CMS_OIDC_CLIENT_ID: ${CMS_OIDC_CLIENT_ID:?cms oidc client id required}
CMS_OIDC_CLIENT_SECRET: ${CMS_OIDC_CLIENT_SECRET:?cms oidc client secret required} CMS_OIDC_CLIENT_SECRET: ${CMS_OIDC_CLIENT_SECRET:?cms oidc client secret required}
CMS_OIDC_REDIRECT_URI: ${CMS_OIDC_REDIRECT_URI:?cms oidc redirect uri required} CMS_OIDC_REDIRECT_URI: ${CMS_OIDC_REDIRECT_URI:?cms oidc redirect uri required}

View File

@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { dirname } from "node:path"; import { dirname } from "node:path";
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto"; import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { createRemoteJWKSet, jwtVerify } from "jose"; import { createLocalJWKSet, jwtVerify } from "jose";
const cmsRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const cmsRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const port = Number(process.env.PORT || 8090); const port = Number(process.env.PORT || 8090);
@ -210,6 +210,9 @@ const authConfig = {
enabled: boolEnv(process.env.CMS_AUTH_ENABLED, false), enabled: boolEnv(process.env.CMS_AUTH_ENABLED, false),
baseUrl: String(process.env.CMS_BASE_URL || "").trim(), baseUrl: String(process.env.CMS_BASE_URL || "").trim(),
issuer: trailingSlashUrl(process.env.CMS_OIDC_ISSUER), issuer: trailingSlashUrl(process.env.CMS_OIDC_ISSUER),
internalBaseUrl: String(process.env.CMS_OIDC_INTERNAL_BASE_URL || "").trim(),
internalHost: String(process.env.CMS_OIDC_INTERNAL_HOST || "").trim(),
internalProto: String(process.env.CMS_OIDC_INTERNAL_PROTO || "").trim(),
clientId: String(process.env.CMS_OIDC_CLIENT_ID || "").trim(), clientId: String(process.env.CMS_OIDC_CLIENT_ID || "").trim(),
clientSecret: String(process.env.CMS_OIDC_CLIENT_SECRET || "").trim(), clientSecret: String(process.env.CMS_OIDC_CLIENT_SECRET || "").trim(),
redirectUri: String(process.env.CMS_OIDC_REDIRECT_URI || "").trim(), redirectUri: String(process.env.CMS_OIDC_REDIRECT_URI || "").trim(),
@ -231,6 +234,7 @@ let discoveryCache = null;
let discoveryPromise = null; let discoveryPromise = null;
let jwksCache = null; let jwksCache = null;
let jwksUriCache = ""; let jwksUriCache = "";
let jwksPromise = null;
function randomToken(byteLength = 32) { function randomToken(byteLength = 32) {
return randomBytes(byteLength).toString("base64url"); return randomBytes(byteLength).toString("base64url");
@ -353,14 +357,56 @@ function assertAuthConfigured() {
} }
} }
function oidcInternalFetchUrl(rawUrl) {
const publicUrl = new URL(rawUrl);
if (!authConfig.internalBaseUrl) return publicUrl;
const internalBaseUrl = new URL(authConfig.internalBaseUrl);
internalBaseUrl.pathname = publicUrl.pathname;
internalBaseUrl.search = publicUrl.search;
internalBaseUrl.hash = "";
return internalBaseUrl;
}
function oidcFetchHeaders(headers) {
const nextHeaders = new Headers(headers || {});
if (!authConfig.internalBaseUrl) return nextHeaders;
const publicIssuer = new URL(authConfig.issuer);
const forwardedHost = authConfig.internalHost || publicIssuer.host;
const forwardedProto = authConfig.internalProto || publicIssuer.protocol.replace(/:$/, "");
nextHeaders.set("host", forwardedHost);
nextHeaders.set("x-forwarded-host", forwardedHost);
nextHeaders.set("x-forwarded-proto", forwardedProto);
return nextHeaders;
}
async function fetchOidc(rawUrl, options = {}) {
return fetch(oidcInternalFetchUrl(rawUrl), {
...options,
headers: oidcFetchHeaders(options.headers),
});
}
async function fetchOidcJson(rawUrl, options = {}) {
const response = await fetchOidc(rawUrl, {
...options,
headers: {
accept: "application/json",
...(options.headers || {}),
},
});
const payload = await response.json().catch(() => null);
return { response, payload };
}
async function loadOidcDiscovery() { async function loadOidcDiscovery() {
assertAuthConfigured(); assertAuthConfigured();
if (discoveryCache) return discoveryCache; if (discoveryCache) return discoveryCache;
if (!discoveryPromise) { if (!discoveryPromise) {
discoveryPromise = (async () => { discoveryPromise = (async () => {
const wellKnownUrl = new URL(".well-known/openid-configuration", authConfig.issuer); const wellKnownUrl = new URL(".well-known/openid-configuration", authConfig.issuer);
const response = await fetch(wellKnownUrl); const { response, payload } = await fetchOidcJson(wellKnownUrl);
const payload = await response.json().catch(() => null);
if (!response.ok || !payload) { if (!response.ok || !payload) {
throw new Error(`Не удалось загрузить OIDC discovery: ${response.status}`); throw new Error(`Не удалось загрузить OIDC discovery: ${response.status}`);
} }
@ -373,12 +419,24 @@ async function loadOidcDiscovery() {
return discoveryPromise; return discoveryPromise;
} }
function jwksForDiscovery(discovery) { async function jwksForDiscovery(discovery) {
if (!jwksCache || jwksUriCache !== discovery.jwks_uri) { if (jwksCache && jwksUriCache === discovery.jwks_uri) return jwksCache;
if (!jwksPromise || jwksUriCache !== discovery.jwks_uri) {
jwksUriCache = discovery.jwks_uri; jwksUriCache = discovery.jwks_uri;
jwksCache = createRemoteJWKSet(new URL(discovery.jwks_uri)); jwksPromise = (async () => {
const { response, payload } = await fetchOidcJson(discovery.jwks_uri);
if (!response.ok || !payload) {
throw new Error(`Не удалось загрузить OIDC JWKS: ${response.status}`);
}
jwksCache = createLocalJWKSet(payload);
return jwksCache;
})().finally(() => {
jwksPromise = null;
});
} }
return jwksCache;
return jwksPromise;
} }
async function exchangeAuthorizationCode(code, codeVerifier, redirectUri) { async function exchangeAuthorizationCode(code, codeVerifier, redirectUri) {
@ -390,7 +448,7 @@ async function exchangeAuthorizationCode(code, codeVerifier, redirectUri) {
code_verifier: codeVerifier, code_verifier: codeVerifier,
}); });
const credentials = Buffer.from(`${encodeURIComponent(authConfig.clientId)}:${encodeURIComponent(authConfig.clientSecret)}`).toString("base64"); const credentials = Buffer.from(`${encodeURIComponent(authConfig.clientId)}:${encodeURIComponent(authConfig.clientSecret)}`).toString("base64");
const response = await fetch(discovery.token_endpoint, { const response = await fetchOidc(discovery.token_endpoint, {
method: "POST", method: "POST",
headers: { headers: {
authorization: `Basic ${credentials}`, authorization: `Basic ${credentials}`,
@ -432,7 +490,7 @@ async function verifyTokens(tokens, pendingLogin) {
const discovery = await loadOidcDiscovery(); const discovery = await loadOidcDiscovery();
const issuer = discovery.issuer || authConfig.issuer.replace(/\/?$/, "/"); const issuer = discovery.issuer || authConfig.issuer.replace(/\/?$/, "/");
const { payload } = await jwtVerify(tokens.id_token, jwksForDiscovery(discovery), { const { payload } = await jwtVerify(tokens.id_token, await jwksForDiscovery(discovery), {
issuer, issuer,
audience: authConfig.clientId, audience: authConfig.clientId,
}); });