ФУНКЦИИ - NODEDC LAUNCHER: direct auth redirect
This commit is contained in:
parent
a1fb2d0f83
commit
a1e8cacd88
|
|
@ -9,3 +9,4 @@ yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
public/storage/backups/
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const maxStorageJsonBodyBytes = "260mb";
|
||||||
const pendingLoginTtlMs = 10 * 60 * 1000;
|
const pendingLoginTtlMs = 10 * 60 * 1000;
|
||||||
const sessionTtlMs = 12 * 60 * 60 * 1000;
|
const sessionTtlMs = 12 * 60 * 60 * 1000;
|
||||||
const oidcStateCookieName = "nodedc_oidc_state";
|
const oidcStateCookieName = "nodedc_oidc_state";
|
||||||
|
const maxOidcStateCookieEntries = 8;
|
||||||
const sessionCookieName = "nodedc_session";
|
const sessionCookieName = "nodedc_session";
|
||||||
|
|
||||||
loadEnvFiles([
|
loadEnvFiles([
|
||||||
|
|
@ -66,7 +67,7 @@ app.get("/auth/login", asyncRoute(async (req, res) => {
|
||||||
expiresAt: Date.now() + pendingLoginTtlMs,
|
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);
|
const authorizationUrl = new URL(discovery.authorization_endpoint);
|
||||||
authorizationUrl.searchParams.set("response_type", "code");
|
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 code = typeof req.query.code === "string" ? req.query.code : null;
|
||||||
const state = typeof req.query.state === "string" ? req.query.state : 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) {
|
if (!code || !state || !cookieStates.includes(state)) {
|
||||||
throw new Error("OIDC callback state validation failed");
|
res.clearCookie(oidcStateCookieName, clearCookieOptions());
|
||||||
|
res.redirect("/auth/login?returnTo=/");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingLogin = pendingLogins.get(state);
|
const pendingLogin = pendingLogins.get(state);
|
||||||
pendingLogins.delete(state);
|
pendingLogins.delete(state);
|
||||||
res.clearCookie(oidcStateCookieName, clearCookieOptions());
|
setOidcStateCookie(res, cookieStates.filter((cookieState) => cookieState !== state));
|
||||||
|
|
||||||
if (!pendingLogin || pendingLogin.expiresAt < Date.now()) {
|
if (!pendingLogin || pendingLogin.expiresAt < Date.now()) {
|
||||||
throw new Error("OIDC login state expired");
|
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) {
|
function parseCookies(cookieHeader) {
|
||||||
if (!cookieHeader) return {};
|
if (!cookieHeader) return {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ export function LauncherApp() {
|
||||||
const [adminOpen, setAdminOpen] = useState(false);
|
const [adminOpen, setAdminOpen] = useState(false);
|
||||||
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
|
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
|
||||||
const [authApps, setAuthApps] = useState<LauncherAuthApp[] | null>(null);
|
const [authApps, setAuthApps] = useState<LauncherAuthApp[] | null>(null);
|
||||||
const [authError, setAuthError] = useState<string | null>(null);
|
|
||||||
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
||||||
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
|
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
|
||||||
|
|
||||||
|
|
@ -160,7 +159,6 @@ export function LauncherApp() {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
setAuthSession(session);
|
setAuthSession(session);
|
||||||
setAuthError(null);
|
|
||||||
|
|
||||||
if (!session.authenticated) {
|
if (!session.authenticated) {
|
||||||
setAuthApps([]);
|
setAuthApps([]);
|
||||||
|
|
@ -178,7 +176,7 @@ export function LauncherApp() {
|
||||||
|
|
||||||
setAuthSession({ authenticated: false, loginUrl: "/auth/login" });
|
setAuthSession({ authenticated: false, loginUrl: "/auth/login" });
|
||||||
setAuthApps([]);
|
setAuthApps([]);
|
||||||
setAuthError(error instanceof Error ? error.message : "Не удалось проверить сессию платформы");
|
console.warn(error instanceof Error ? error.message : "Не удалось проверить сессию платформы");
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -186,6 +184,12 @@ export function LauncherApp() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authSession || authSession.authenticated) return;
|
||||||
|
|
||||||
|
window.location.replace(authSession.loginUrl || "/auth/login");
|
||||||
|
}, [authSession]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authSession?.authenticated) return;
|
if (!authSession?.authenticated) return;
|
||||||
|
|
||||||
|
|
@ -247,7 +251,6 @@ export function LauncherApp() {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
setAuthSession(nextSession);
|
setAuthSession(nextSession);
|
||||||
setAuthError(null);
|
|
||||||
|
|
||||||
if (!nextSession.authenticated) {
|
if (!nextSession.authenticated) {
|
||||||
setAuthApps([]);
|
setAuthApps([]);
|
||||||
|
|
@ -473,7 +476,7 @@ export function LauncherApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authSession.authenticated) {
|
if (!authSession.authenticated) {
|
||||||
return <AuthStateScreen title="Вход на платформу NODE.DC" description="Войдите, чтобы открыть рабочую область и доступы." error={authError} loginUrl={authSession.loginUrl} />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue