Route CMS OIDC calls through internal proxy
This commit is contained in:
parent
88ad0557ad
commit
a836d3639b
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue