From a1e8cacd888ff837fda9b92998766220ed78cdd6 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 4 May 2026 21:39:56 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=A3=D0=9D=D0=9A=D0=A6=D0=98=D0=98=20-?= =?UTF-8?q?=20NODEDC=20LAUNCHER:=20direct=20auth=20redirect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + server/dev-server.mjs | 43 ++++++++++++++++++++++++++++++++++++----- src/app/LauncherApp.tsx | 13 ++++++++----- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 6d02569..8a3adc5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* *.tsbuildinfo +public/storage/backups/ diff --git a/server/dev-server.mjs b/server/dev-server.mjs index 119c1ec..0b8cb3c 100644 --- a/server/dev-server.mjs +++ b/server/dev-server.mjs @@ -16,6 +16,7 @@ const maxStorageJsonBodyBytes = "260mb"; const pendingLoginTtlMs = 10 * 60 * 1000; const sessionTtlMs = 12 * 60 * 60 * 1000; const oidcStateCookieName = "nodedc_oidc_state"; +const maxOidcStateCookieEntries = 8; const sessionCookieName = "nodedc_session"; loadEnvFiles([ @@ -66,7 +67,7 @@ app.get("/auth/login", asyncRoute(async (req, res) => { expiresAt: Date.now() + pendingLoginTtlMs, }); - res.cookie(oidcStateCookieName, state, cookieOptions(pendingLoginTtlMs)); + setOidcStateCookie(res, [state, ...getValidOidcCookieStates(req)].slice(0, maxOidcStateCookieEntries)); const authorizationUrl = new URL(discovery.authorization_endpoint); authorizationUrl.searchParams.set("response_type", "code"); @@ -97,15 +98,17 @@ app.get("/auth/callback", asyncRoute(async (req, res) => { const code = typeof req.query.code === "string" ? req.query.code : null; const state = typeof req.query.state === "string" ? req.query.state : null; - const cookieState = parseCookies(req.headers.cookie)[oidcStateCookieName]; + const cookieStates = getValidOidcCookieStates(req); - if (!code || !state || state !== cookieState) { - throw new Error("OIDC callback state validation failed"); + if (!code || !state || !cookieStates.includes(state)) { + res.clearCookie(oidcStateCookieName, clearCookieOptions()); + res.redirect("/auth/login?returnTo=/"); + return; } const pendingLogin = pendingLogins.get(state); pendingLogins.delete(state); - res.clearCookie(oidcStateCookieName, clearCookieOptions()); + setOidcStateCookie(res, cookieStates.filter((cookieState) => cookieState !== state)); if (!pendingLogin || pendingLogin.expiresAt < Date.now()) { throw new Error("OIDC login state expired"); @@ -1073,6 +1076,36 @@ function pruneExpiredState() { } } +function getValidOidcCookieStates(req) { + const rawValue = parseCookies(req.headers.cookie)[oidcStateCookieName]; + + if (!rawValue) return []; + + const seen = new Set(); + + return rawValue + .split(".") + .filter((state) => /^[A-Za-z0-9_-]{32,256}$/.test(state)) + .filter((state) => { + if (seen.has(state)) return false; + seen.add(state); + return true; + }) + .filter((state) => { + const pendingLogin = pendingLogins.get(state); + return Boolean(pendingLogin && pendingLogin.expiresAt >= Date.now()); + }); +} + +function setOidcStateCookie(res, states) { + if (!states.length) { + res.clearCookie(oidcStateCookieName, clearCookieOptions()); + return; + } + + res.cookie(oidcStateCookieName, states.join("."), cookieOptions(pendingLoginTtlMs)); +} + function parseCookies(cookieHeader) { if (!cookieHeader) return {}; diff --git a/src/app/LauncherApp.tsx b/src/app/LauncherApp.tsx index 57aec22..032786e 100644 --- a/src/app/LauncherApp.tsx +++ b/src/app/LauncherApp.tsx @@ -62,7 +62,6 @@ export function LauncherApp() { const [adminOpen, setAdminOpen] = useState(false); const [authSession, setAuthSession] = useState(null); const [authApps, setAuthApps] = useState(null); - const [authError, setAuthError] = useState(null); const [profileSettingsOpen, setProfileSettingsOpen] = useState(false); const [pendingAccessAssignments, setPendingAccessAssignments] = useState>({}); @@ -160,7 +159,6 @@ export function LauncherApp() { if (!isMounted) return; setAuthSession(session); - setAuthError(null); if (!session.authenticated) { setAuthApps([]); @@ -178,7 +176,7 @@ export function LauncherApp() { setAuthSession({ authenticated: false, loginUrl: "/auth/login" }); setAuthApps([]); - setAuthError(error instanceof Error ? error.message : "Не удалось проверить сессию платформы"); + console.warn(error instanceof Error ? error.message : "Не удалось проверить сессию платформы"); }); return () => { @@ -186,6 +184,12 @@ export function LauncherApp() { }; }, []); + useEffect(() => { + if (!authSession || authSession.authenticated) return; + + window.location.replace(authSession.loginUrl || "/auth/login"); + }, [authSession]); + useEffect(() => { if (!authSession?.authenticated) return; @@ -247,7 +251,6 @@ export function LauncherApp() { if (!isMounted) return; setAuthSession(nextSession); - setAuthError(null); if (!nextSession.authenticated) { setAuthApps([]); @@ -473,7 +476,7 @@ export function LauncherApp() { } if (!authSession.authenticated) { - return ; + return null; } return (