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_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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue