diff --git a/README.md b/README.md index 1c47b7d..498bbd8 100644 --- a/README.md +++ b/README.md @@ -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`. +CMS обращается к OIDC discovery внутри compose-сети через host-alias Authentik-домена (`CMS_AUTH_DOMAIN`), а не напрямую в контейнер `authentik-server`. Это сохраняет тот же Host/issuer-контекст, который видит браузер. + ## Synology Для Synology используется отдельный env-шаблон `infra/.env.synology.example`. Рабочий файл должен лежать рядом как `infra/.env.synology` и не должен попадать в artifact. diff --git a/infra/.env.example b/infra/.env.example index 266bfd3..89479ed 100644 --- a/infra/.env.example +++ b/infra/.env.example @@ -40,7 +40,6 @@ CMS_BOOTSTRAP_FORCE_PASSWORD=false 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://authentik-server:9000 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 diff --git a/infra/.env.synology.example b/infra/.env.synology.example index 2f872f7..754c77b 100644 --- a/infra/.env.synology.example +++ b/infra/.env.synology.example @@ -41,7 +41,6 @@ CMS_BOOTSTRAP_FORCE_PASSWORD=false 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://authentik-server:9000 CMS_OIDC_CLIENT_ID=dc-cms CMS_OIDC_CLIENT_SECRET=replace-with-random-secret CMS_OIDC_REDIRECT_URI=https://cms.dcserve.ru/auth/callback diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 97345b9..8c4944d 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -38,7 +38,7 @@ 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:-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_SECRET: ${CMS_OIDC_CLIENT_SECRET:?cms oidc client secret required} CMS_OIDC_REDIRECT_URI: ${CMS_OIDC_REDIRECT_URI:?cms oidc redirect uri required} diff --git a/server/admin-server.mjs b/server/admin-server.mjs index 9bbc1cf..ef495f9 100644 --- a/server/admin-server.mjs +++ b/server/admin-server.mjs @@ -368,12 +368,14 @@ function oidcInternalFetchUrl(rawUrl) { async function fetchOidc(rawUrl, options = {}) { return fetch(oidcInternalFetchUrl(rawUrl), { + redirect: "manual", ...options, headers: options.headers, }); } async function fetchOidcJson(rawUrl, options = {}) { + const fetchUrl = oidcInternalFetchUrl(rawUrl); const response = await fetchOidc(rawUrl, { ...options, headers: { @@ -381,8 +383,31 @@ async function fetchOidcJson(rawUrl, options = {}) { ...(options.headers || {}), }, }); - const payload = await response.json().catch(() => null); - return { response, payload }; + const body = await response.text().catch(() => ""); + 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) { @@ -416,9 +441,9 @@ async function loadOidcDiscovery() { if (!discoveryPromise) { discoveryPromise = (async () => { 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) { - throw new Error(`Не удалось загрузить OIDC discovery: ${response.status}`); + throw new Error(oidcJsonError("Не удалось загрузить OIDC discovery", response, body, fetchUrl)); } discoveryCache = normalizeDiscovery(payload); return discoveryCache; @@ -435,9 +460,9 @@ async function jwksForDiscovery(discovery) { if (!jwksPromise || jwksUriCache !== discovery.jwks_uri) { jwksUriCache = discovery.jwks_uri; jwksPromise = (async () => { - const { response, payload } = await fetchOidcJson(discovery.jwks_uri); + const { response, payload, body, fetchUrl } = await fetchOidcJson(discovery.jwks_uri); if (!response.ok || !payload) { - throw new Error(`Не удалось загрузить OIDC JWKS: ${response.status}`); + throw new Error(oidcJsonError("Не удалось загрузить OIDC JWKS", response, body, fetchUrl)); } jwksCache = createLocalJWKSet(payload); return jwksCache;