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_BASE_URL=http://cms.local.nodedc:8210
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_SECRET=replace-with-random-secret
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_BASE_URL=https://cms.dcserve.ru
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_SECRET=replace-with-random-secret
CMS_OIDC_REDIRECT_URI=https://cms.dcserve.ru/auth/callback

View File

@ -38,6 +38,9 @@ services:
SITE_ROOT: /sites/nodedc
CMS_AUTH_ENABLED: ${CMS_AUTH_ENABLED:-true}
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_SECRET: ${CMS_OIDC_CLIENT_SECRET:?cms oidc client secret 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 { dirname } from "node:path";
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 port = Number(process.env.PORT || 8090);
@ -210,6 +210,9 @@ const authConfig = {
enabled: boolEnv(process.env.CMS_AUTH_ENABLED, false),
baseUrl: String(process.env.CMS_BASE_URL || "").trim(),
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(),
clientSecret: String(process.env.CMS_OIDC_CLIENT_SECRET || "").trim(),
redirectUri: String(process.env.CMS_OIDC_REDIRECT_URI || "").trim(),
@ -231,6 +234,7 @@ let discoveryCache = null;
let discoveryPromise = null;
let jwksCache = null;
let jwksUriCache = "";
let jwksPromise = null;
function randomToken(byteLength = 32) {
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() {
assertAuthConfigured();
if (discoveryCache) return discoveryCache;
if (!discoveryPromise) {
discoveryPromise = (async () => {
const wellKnownUrl = new URL(".well-known/openid-configuration", authConfig.issuer);
const response = await fetch(wellKnownUrl);
const payload = await response.json().catch(() => null);
const { response, payload } = await fetchOidcJson(wellKnownUrl);
if (!response.ok || !payload) {
throw new Error(`Не удалось загрузить OIDC discovery: ${response.status}`);
}
@ -373,12 +419,24 @@ async function loadOidcDiscovery() {
return discoveryPromise;
}
function jwksForDiscovery(discovery) {
if (!jwksCache || jwksUriCache !== discovery.jwks_uri) {
async function jwksForDiscovery(discovery) {
if (jwksCache && jwksUriCache === discovery.jwks_uri) return jwksCache;
if (!jwksPromise || 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 jwksPromise;
}
async function exchangeAuthorizationCode(code, codeVerifier, redirectUri) {
@ -390,7 +448,7 @@ async function exchangeAuthorizationCode(code, codeVerifier, redirectUri) {
code_verifier: codeVerifier,
});
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",
headers: {
authorization: `Basic ${credentials}`,
@ -432,7 +490,7 @@ async function verifyTokens(tokens, pendingLogin) {
const discovery = await loadOidcDiscovery();
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,
audience: authConfig.clientId,
});