ФУНКЦИИ - 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 maxOidcStateCookieEntries = 8;
|
||||
const sessionCookieName = "nodedc_session";
|
||||
const noStoreCacheControl = "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
|
||||
loadEnvFiles([
|
||||
process.env.NODEDC_PLATFORM_ENV,
|
||||
|
|
@ -37,6 +38,13 @@ let discoveryCache = null;
|
|||
let jwksCache = null;
|
||||
|
||||
app.disable("x-powered-by");
|
||||
app.use((req, res, next) => {
|
||||
if (shouldDisableHttpCache(req)) {
|
||||
lockNoStoreHeaders(res);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
app.use(express.json({ limit: maxStorageJsonBodyBytes }));
|
||||
|
||||
app.get("/healthz", (_req, res) => {
|
||||
|
|
@ -85,6 +93,10 @@ app.get("/auth/login", asyncRoute(async (req, res) => {
|
|||
authorizationUrl.searchParams.set("prompt", prompt);
|
||||
}
|
||||
|
||||
if (prompt === "login") {
|
||||
authorizationUrl.searchParams.set("max_age", "0");
|
||||
}
|
||||
|
||||
res.redirect(authorizationUrl.toString());
|
||||
}));
|
||||
|
||||
|
|
@ -175,24 +187,22 @@ app.get("/auth/logout", asyncRoute(async (req, res) => {
|
|||
const discovery = await getOidcDiscovery();
|
||||
const endSessionEndpoint = discovery.end_session_endpoint;
|
||||
const loginRedirectUrl = buildLoginRedirectUrl(returnTo, { forceLogin: true });
|
||||
const postLogoutRedirectUrl = buildLoggedOutRedirectUrl();
|
||||
|
||||
if (!endSessionEndpoint) {
|
||||
setNoStore(res);
|
||||
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), loginRedirectUrl));
|
||||
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), null, loginRedirectUrl));
|
||||
return;
|
||||
}
|
||||
|
||||
const logoutUrl = new URL(endSessionEndpoint);
|
||||
logoutUrl.searchParams.set("client_id", config.clientId);
|
||||
logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutRedirectUrl);
|
||||
|
||||
if (session?.tokenSet.idToken) {
|
||||
logoutUrl.searchParams.set("id_token_hint", session.tokenSet.idToken);
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
|
@ -568,7 +578,6 @@ function readConfig() {
|
|||
clientId,
|
||||
clientSecret,
|
||||
redirectUri: process.env.LAUNCHER_OIDC_REDIRECT_URI ?? `${appBaseUrl}/auth/callback`,
|
||||
loggedOutRedirectUri: process.env.LAUNCHER_OIDC_LOGGED_OUT_REDIRECT_URI ?? `${appBaseUrl}/auth/logged-out`,
|
||||
appBaseUrl,
|
||||
scope: process.env.LAUNCHER_OIDC_SCOPE ?? "openid email profile groups offline_access",
|
||||
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 identityProviderLogoutUrlJson = JSON.stringify(identityProviderLogoutUrl);
|
||||
const redirectUrlJson = JSON.stringify(finalRedirectUrl);
|
||||
|
||||
return `<!doctype html>
|
||||
|
|
@ -979,6 +989,7 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
|
|||
localStorage.setItem("nodedc:platform-session-event", JSON.stringify(eventPayload));
|
||||
} catch {}
|
||||
const logoutUrls = ${logoutUrlsJson};
|
||||
const identityProviderLogoutUrl = ${identityProviderLogoutUrlJson};
|
||||
const finalRedirectUrl = ${redirectUrlJson};
|
||||
for (const logoutUrl of logoutUrls) {
|
||||
fetch(logoutUrl, { mode: "no-cors", credentials: "include", keepalive: true }).catch(() => undefined);
|
||||
|
|
@ -986,7 +997,22 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
|
|||
image.referrerPolicy = "no-referrer";
|
||||
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>
|
||||
</body>
|
||||
</html>`;
|
||||
|
|
@ -1375,8 +1401,45 @@ function clearCookieOptions() {
|
|||
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) {
|
||||
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("Expires", "0");
|
||||
}
|
||||
|
|
@ -1396,11 +1459,6 @@ function buildLoginRedirectUrl(returnTo, { forceLogin = false } = {}) {
|
|||
return loginUrl.toString();
|
||||
}
|
||||
|
||||
function buildLoggedOutRedirectUrl() {
|
||||
const loggedOutUrl = new URL(config.loggedOutRedirectUri);
|
||||
return loggedOutUrl.toString();
|
||||
}
|
||||
|
||||
function randomBase64Url(size) {
|
||||
return randomBytes(size).toString("base64url");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue