Route CMS OIDC through Authentik host alias
This commit is contained in:
parent
7a96aaaaf1
commit
33ab11ab08
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue