Route CMS OIDC through Authentik host alias

This commit is contained in:
DCCONSTRUCTIONS 2026-07-06 11:02:25 +03:00
parent 7a96aaaaf1
commit 33ab11ab08
5 changed files with 34 additions and 9 deletions

View File

@ -45,6 +45,8 @@ docker compose --env-file infra/.env -f infra/docker-compose.yml up -d --build
Compose-сервис `authentik-bootstrap` создает OIDC-приложение `dc-cms`, группы `dc-cms:*` и стартового администратора из `infra/.env`. После первичного входа временный пароль нужно заменить в Authentik; следующие deploy не перетрут пароль, пока `CMS_BOOTSTRAP_FORCE_PASSWORD=false`. Compose-сервис `authentik-bootstrap` создает OIDC-приложение `dc-cms`, группы `dc-cms:*` и стартового администратора из `infra/.env`. После первичного входа временный пароль нужно заменить в Authentik; следующие deploy не перетрут пароль, пока `CMS_BOOTSTRAP_FORCE_PASSWORD=false`.
CMS обращается к OIDC discovery внутри compose-сети через host-alias Authentik-домена (`CMS_AUTH_DOMAIN`), а не напрямую в контейнер `authentik-server`. Это сохраняет тот же Host/issuer-контекст, который видит браузер.
## Synology ## Synology
Для Synology используется отдельный env-шаблон `infra/.env.synology.example`. Рабочий файл должен лежать рядом как `infra/.env.synology` и не должен попадать в artifact. Для Synology используется отдельный env-шаблон `infra/.env.synology.example`. Рабочий файл должен лежать рядом как `infra/.env.synology` и не должен попадать в artifact.

View File

@ -40,7 +40,6 @@ CMS_BOOTSTRAP_FORCE_PASSWORD=false
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://authentik-server:9000
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

@ -41,7 +41,6 @@ CMS_BOOTSTRAP_FORCE_PASSWORD=false
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://authentik-server:9000
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,7 +38,7 @@ 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:-http://authentik-server:9000} CMS_OIDC_INTERNAL_BASE_URL: http://${CMS_AUTH_DOMAIN:-cms-auth.local.nodedc}
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

@ -368,12 +368,14 @@ function oidcInternalFetchUrl(rawUrl) {
async function fetchOidc(rawUrl, options = {}) { async function fetchOidc(rawUrl, options = {}) {
return fetch(oidcInternalFetchUrl(rawUrl), { return fetch(oidcInternalFetchUrl(rawUrl), {
redirect: "manual",
...options, ...options,
headers: options.headers, headers: options.headers,
}); });
} }
async function fetchOidcJson(rawUrl, options = {}) { async function fetchOidcJson(rawUrl, options = {}) {
const fetchUrl = oidcInternalFetchUrl(rawUrl);
const response = await fetchOidc(rawUrl, { const response = await fetchOidc(rawUrl, {
...options, ...options,
headers: { headers: {
@ -381,8 +383,31 @@ async function fetchOidcJson(rawUrl, options = {}) {
...(options.headers || {}), ...(options.headers || {}),
}, },
}); });
const payload = await response.json().catch(() => null); const body = await response.text().catch(() => "");
return { response, payload }; let payload = null;
try {
payload = body ? JSON.parse(body) : null;
} catch {
payload = null;
}
return { response, payload, body, fetchUrl };
}
function oidcJsonError(label, response, body, fetchUrl) {
const contentType = response.headers.get("content-type") || "";
const location = response.headers.get("location") || "";
const bodyPreview = String(body || "")
.replace(/\s+/g, " ")
.trim()
.slice(0, 180);
const parts = [
`${label}: ${response.status}`,
`url=${fetchUrl}`,
contentType ? `content-type=${contentType}` : "",
location ? `location=${location}` : "",
bodyPreview ? `body=${bodyPreview}` : "",
].filter(Boolean);
return parts.join("; ");
} }
function publicOidcUrl(rawUrl) { function publicOidcUrl(rawUrl) {
@ -416,9 +441,9 @@ async function loadOidcDiscovery() {
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, payload } = await fetchOidcJson(wellKnownUrl); const { response, payload, body, fetchUrl } = await fetchOidcJson(wellKnownUrl);
if (!response.ok || !payload) { if (!response.ok || !payload) {
throw new Error(`Не удалось загрузить OIDC discovery: ${response.status}`); throw new Error(oidcJsonError("Не удалось загрузить OIDC discovery", response, body, fetchUrl));
} }
discoveryCache = normalizeDiscovery(payload); discoveryCache = normalizeDiscovery(payload);
return discoveryCache; return discoveryCache;
@ -435,9 +460,9 @@ async function jwksForDiscovery(discovery) {
if (!jwksPromise || jwksUriCache !== discovery.jwks_uri) { if (!jwksPromise || jwksUriCache !== discovery.jwks_uri) {
jwksUriCache = discovery.jwks_uri; jwksUriCache = discovery.jwks_uri;
jwksPromise = (async () => { jwksPromise = (async () => {
const { response, payload } = await fetchOidcJson(discovery.jwks_uri); const { response, payload, body, fetchUrl } = await fetchOidcJson(discovery.jwks_uri);
if (!response.ok || !payload) { if (!response.ok || !payload) {
throw new Error(`Не удалось загрузить OIDC JWKS: ${response.status}`); throw new Error(oidcJsonError("Не удалось загрузить OIDC JWKS", response, body, fetchUrl));
} }
jwksCache = createLocalJWKSet(payload); jwksCache = createLocalJWKSet(payload);
return jwksCache; return jwksCache;