ФУНКЦИИ - NODEDC LAUNCHER: fix global logout handoff

This commit is contained in:
DCCONSTRUCTIONS 2026-05-04 22:22:08 +03:00
parent 8e6d2cea39
commit ab1e0856d6
2 changed files with 71 additions and 6 deletions

View File

@ -148,27 +148,31 @@ app.get("/auth/logout", asyncRoute(async (req, res) => {
res.clearCookie(sessionCookieName, clearCookieOptions()); res.clearCookie(sessionCookieName, clearCookieOptions());
if (!globalLogout || !config.oidcConfigured) { if (!globalLogout || !config.oidcConfigured) {
setNoStore(res);
res.redirect(returnTo); res.redirect(returnTo);
return; return;
} }
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 });
if (!endSessionEndpoint) { if (!endSessionEndpoint) {
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), new URL(returnTo, config.appBaseUrl).toString())); setNoStore(res);
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), 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", new URL(returnTo, config.appBaseUrl).toString()); logoutUrl.searchParams.set("post_logout_redirect_uri", loginRedirectUrl);
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);
} }
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), logoutUrl.toString())); setNoStore(res);
res.type("html").send(renderGlobalLogoutPage(getFrontchannelLogoutUrls(), loginRedirectUrl, logoutUrl.toString()));
})); }));
app.get("/api/me", (req, res) => { app.get("/api/me", (req, res) => {
@ -915,9 +919,10 @@ function normalizeLogoutUrl(value) {
} }
} }
function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) { function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl, identityLogoutUrl = null) {
const logoutUrlsJson = JSON.stringify(frontchannelLogoutUrls); const logoutUrlsJson = JSON.stringify(frontchannelLogoutUrls);
const redirectUrlJson = JSON.stringify(finalRedirectUrl); const redirectUrlJson = JSON.stringify(finalRedirectUrl);
const identityLogoutUrlJson = JSON.stringify(identityLogoutUrl);
return `<!doctype html> return `<!doctype html>
<html lang="ru"> <html lang="ru">
@ -941,13 +946,20 @@ function renderGlobalLogoutPage(frontchannelLogoutUrls, finalRedirectUrl) {
<script> <script>
const logoutUrls = ${logoutUrlsJson}; const logoutUrls = ${logoutUrlsJson};
const finalRedirectUrl = ${redirectUrlJson}; const finalRedirectUrl = ${redirectUrlJson};
const identityLogoutUrl = ${identityLogoutUrlJson};
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);
const image = new Image(); const image = new Image();
image.referrerPolicy = "no-referrer"; image.referrerPolicy = "no-referrer";
image.src = logoutUrl; image.src = logoutUrl;
} }
window.setTimeout(() => window.location.replace(finalRedirectUrl), 700); if (identityLogoutUrl) {
fetch(identityLogoutUrl, { mode: "no-cors", credentials: "include", keepalive: true }).catch(() => undefined);
const identityImage = new Image();
identityImage.referrerPolicy = "no-referrer";
identityImage.src = identityLogoutUrl;
}
window.setTimeout(() => window.location.replace(finalRedirectUrl), 900);
</script> </script>
</body> </body>
</html>`; </html>`;
@ -1251,6 +1263,27 @@ function clearCookieOptions() {
return options; return options;
} }
function setNoStore(res) {
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
}
function buildLoginRedirectUrl(returnTo, { forceLogin = false } = {}) {
const loginUrl = new URL("/auth/login", config.appBaseUrl);
const cleanReturnTo = sanitizeReturnTo(returnTo);
if (forceLogin) {
loginUrl.searchParams.set("prompt", "login");
}
if (cleanReturnTo !== "/") {
loginUrl.searchParams.set("returnTo", cleanReturnTo);
}
return loginUrl.toString();
}
function randomBase64Url(size) { function randomBase64Url(size) {
return randomBytes(size).toString("base64url"); return randomBytes(size).toString("base64url");
} }

View File

@ -190,6 +190,38 @@ export function LauncherApp() {
window.location.replace(buildLoginRedirectUrl(authSession.loginUrl)); window.location.replace(buildLoginRedirectUrl(authSession.loginUrl));
}, [authSession]); }, [authSession]);
useEffect(() => {
let isMounted = true;
const validateRestoredSession = (event: PageTransitionEvent) => {
if (!event.persisted) return;
fetchAuthSession()
.then((session) => {
if (!isMounted) return;
if (!session.authenticated) {
window.location.replace(buildLoginRedirectUrl(session.loginUrl));
return;
}
setAuthSession(session);
})
.catch(() => {
if (isMounted) {
window.location.replace(buildLoginRedirectUrl("/auth/login"));
}
});
};
window.addEventListener("pageshow", validateRestoredSession);
return () => {
isMounted = false;
window.removeEventListener("pageshow", validateRestoredSession);
};
}, []);
useEffect(() => { useEffect(() => {
if (!authSession?.authenticated) return; if (!authSession?.authenticated) return;
@ -493,7 +525,7 @@ export function LauncherApp() {
onToggleAdmin={() => setAdminOpen((current) => !current)} onToggleAdmin={() => setAdminOpen((current) => !current)}
onOpenShowcase={() => setAdminOpen(false)} onOpenShowcase={() => setAdminOpen(false)}
onOpenProfileSettings={() => setProfileSettingsOpen(true)} onOpenProfileSettings={() => setProfileSettingsOpen(true)}
onLogout={() => window.location.assign(authSession.logoutUrl)} onLogout={() => window.location.replace(authSession.logoutUrl)}
/> />
<main className="launcher-main"> <main className="launcher-main">