ФУНКЦИИ - NODEDC LAUNCHER: harden global logout flow
This commit is contained in:
parent
049b914916
commit
96e6a97d38
|
|
@ -18,6 +18,7 @@ const sessionTtlMs = 12 * 60 * 60 * 1000;
|
||||||
const oidcStateCookieName = "nodedc_oidc_state";
|
const oidcStateCookieName = "nodedc_oidc_state";
|
||||||
const maxOidcStateCookieEntries = 8;
|
const maxOidcStateCookieEntries = 8;
|
||||||
const sessionCookieName = "nodedc_session";
|
const sessionCookieName = "nodedc_session";
|
||||||
|
const noStoreCacheControl = "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||||
|
|
||||||
loadEnvFiles([
|
loadEnvFiles([
|
||||||
process.env.NODEDC_PLATFORM_ENV,
|
process.env.NODEDC_PLATFORM_ENV,
|
||||||
|
|
@ -37,6 +38,13 @@ let discoveryCache = null;
|
||||||
let jwksCache = null;
|
let jwksCache = null;
|
||||||
|
|
||||||
app.disable("x-powered-by");
|
app.disable("x-powered-by");
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (shouldDisableHttpCache(req)) {
|
||||||
|
lockNoStoreHeaders(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
app.use(express.json({ limit: maxStorageJsonBodyBytes }));
|
app.use(express.json({ limit: maxStorageJsonBodyBytes }));
|
||||||
|
|
||||||
app.get("/healthz", (_req, res) => {
|
app.get("/healthz", (_req, res) => {
|
||||||
|
|
@ -85,6 +93,10 @@ app.get("/auth/login", asyncRoute(async (req, res) => {
|
||||||
authorizationUrl.searchParams.set("prompt", prompt);
|
authorizationUrl.searchParams.set("prompt", prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prompt === "login") {
|
||||||
|
authorizationUrl.searchParams.set("max_age", "0");
|
||||||
|
}
|
||||||
|
|
||||||
res.redirect(authorizationUrl.toString());
|
res.redirect(authorizationUrl.toString());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -175,24 +187,22 @@ app.get("/auth/logout", asyncRoute(async (req, res) => {
|
||||||
const discovery = await getOidcDiscovery();
|
const discovery = await getOidcDiscovery();
|
||||||
const endSessionEndpoint = discovery.end_session_endpoint;
|
const endSessionEndpoint = discovery.end_session_endpoint;
|
||||||
const loginRedirectUrl = buildLoginRedirectUrl(returnTo, { forceLogin: true });
|
const loginRedirectUrl = buildLoginRedirectUrl(returnTo, { forceLogin: true });
|
||||||
const postLogoutRedirectUrl = buildLoggedOutRedirectUrl();
|
|
||||||
|
|
||||||
if (!endSessionEndpoint) {
|
if (!endSessionEndpoint) {
|
||||||
setNoStore(res);
|
setNoStore(res);
|
||||||
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), loginRedirectUrl));
|
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), null, loginRedirectUrl));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoutUrl = new URL(endSessionEndpoint);
|
const logoutUrl = new URL(endSessionEndpoint);
|
||||||
logoutUrl.searchParams.set("client_id", config.clientId);
|
logoutUrl.searchParams.set("client_id", config.clientId);
|
||||||
logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutRedirectUrl);
|
|
||||||
|
|
||||||
if (session?.tokenSet.idToken) {
|
if (session?.tokenSet.idToken) {
|
||||||
logoutUrl.searchParams.set("id_token_hint", session.tokenSet.idToken);
|
logoutUrl.searchParams.set("id_token_hint", session.tokenSet.idToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNoStore(res);
|
setNoStore(res);
|
||||||
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), logoutUrl.toString()));
|
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), logoutUrl.toString(), loginRedirectUrl));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.get("/api/me", (req, res) => {
|
app.get("/api/me", (req, res) => {
|
||||||
|
|
@ -568,7 +578,6 @@ function readConfig() {
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
redirectUri: process.env.LAUNCHER_OIDC_REDIRECT_URI ?? `${appBaseUrl}/auth/callback`,
|
redirectUri: process.env.LAUNCHER_OIDC_REDIRECT_URI ?? `${appBaseUrl}/auth/callback`,
|
||||||
loggedOutRedirectUri: process.env.LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI ?? `${appBaseUrl}/auth/logged-out`,
|
|
||||||
appBaseUrl,
|
appBaseUrl,
|
||||||
scope: process.env.LAUNCHER_OIDC_SCOPE ?? "openid email profile groups offline_access",
|
scope: process.env.LAUNCHER_OIDC_SCOPE ?? "openid email profile groups offline_access",
|
||||||
cookieDomain: process.env.LAUNCHER_COOKIE_DOMAIN || undefined,
|
cookieDomain: process.env.LAUNCHER_COOKIE_DOMAIN || undefined,
|
||||||
|
|
@ -940,8 +949,9 @@ function normalizeLogoutUrl(value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
|
function renderGlobalLogoutPage(frontchannelLogoutUrls, identityProviderLogoutUrl, finalRedirectUrl) {
|
||||||
const logoutUrlsJson = JSON.stringify(frontchannelLogoutUrls);
|
const logoutUrlsJson = JSON.stringify(frontchannelLogoutUrls);
|
||||||
|
const identityProviderLogoutUrlJson = JSON.stringify(identityProviderLogoutUrl);
|
||||||
const redirectUrlJson = JSON.stringify(finalRedirectUrl);
|
const redirectUrlJson = JSON.stringify(finalRedirectUrl);
|
||||||
|
|
||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
|
|
@ -979,6 +989,7 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
|
||||||
localStorage.setItem("nodedc:platform-session-event", JSON.stringify(eventPayload));
|
localStorage.setItem("nodedc:platform-session-event", JSON.stringify(eventPayload));
|
||||||
} catch {}
|
} catch {}
|
||||||
const logoutUrls = ${logoutUrlsJson};
|
const logoutUrls = ${logoutUrlsJson};
|
||||||
|
const identityProviderLogoutUrl = ${identityProviderLogoutUrlJson};
|
||||||
const finalRedirectUrl = ${redirectUrlJson};
|
const finalRedirectUrl = ${redirectUrlJson};
|
||||||
for (const logoutUrl of logoutUrls) {
|
for (const logoutUrl of logoutUrls) {
|
||||||
fetch(logoutUrl, { mode: "no-cors", credentials: "include", keepalive: true }).catch(() => undefined);
|
fetch(logoutUrl, { mode: "no-cors", credentials: "include", keepalive: true }).catch(() => undefined);
|
||||||
|
|
@ -986,7 +997,22 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
|
||||||
image.referrerPolicy = "no-referrer";
|
image.referrerPolicy = "no-referrer";
|
||||||
image.src = logoutUrl;
|
image.src = logoutUrl;
|
||||||
}
|
}
|
||||||
window.setTimeout(() => window.location.replace(finalRedirectUrl), 900);
|
if (identityProviderLogoutUrl) {
|
||||||
|
fetch(identityProviderLogoutUrl, { mode: "no-cors", credentials: "include", keepalive: true }).catch(() => undefined);
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
iframe.title = "NODE.DC identity logout";
|
||||||
|
iframe.tabIndex = -1;
|
||||||
|
iframe.setAttribute("aria-hidden", "true");
|
||||||
|
iframe.style.position = "fixed";
|
||||||
|
iframe.style.width = "0";
|
||||||
|
iframe.style.height = "0";
|
||||||
|
iframe.style.opacity = "0";
|
||||||
|
iframe.style.pointerEvents = "none";
|
||||||
|
iframe.style.border = "0";
|
||||||
|
iframe.src = identityProviderLogoutUrl;
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
}
|
||||||
|
window.setTimeout(() => window.location.replace(finalRedirectUrl), 1200);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|
@ -1375,8 +1401,45 @@ function clearCookieOptions() {
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldDisableHttpCache(req) {
|
||||||
|
if (req.path.startsWith("/api/") || req.path.startsWith("/auth/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accept = typeof req.headers.accept === "string" ? req.headers.accept : "";
|
||||||
|
return accept.includes("text/html");
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockNoStoreHeaders(res) {
|
||||||
|
const setHeader = res.setHeader.bind(res);
|
||||||
|
|
||||||
|
setNoStore(res);
|
||||||
|
|
||||||
|
res.setHeader = (name, value) => {
|
||||||
|
const normalizedName = String(name).toLowerCase();
|
||||||
|
|
||||||
|
if (normalizedName === "cache-control") {
|
||||||
|
return setHeader(name, noStoreCacheControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedName === "pragma") {
|
||||||
|
return setHeader(name, "no-cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedName === "expires") {
|
||||||
|
return setHeader(name, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return setHeader(name, value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function setNoStore(res) {
|
function setNoStore(res) {
|
||||||
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0");
|
res.setHeader("Cache-Control", noStoreCacheControl);
|
||||||
res.setHeader("Pragma", "no-cache");
|
res.setHeader("Pragma", "no-cache");
|
||||||
res.setHeader("Expires", "0");
|
res.setHeader("Expires", "0");
|
||||||
}
|
}
|
||||||
|
|
@ -1396,11 +1459,6 @@ function buildLoginRedirectUrl(returnTo, { forceLogin = false } = {}) {
|
||||||
return loginUrl.toString();
|
return loginUrl.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLoggedOutRedirectUrl() {
|
|
||||||
const loggedOutUrl = new URL(config.loggedOutRedirectUri);
|
|
||||||
return loggedOutUrl.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomBase64Url(size) {
|
function randomBase64Url(size) {
|
||||||
return randomBytes(size).toString("base64url");
|
return randomBytes(size).toString("base64url");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue