mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-05-29 21:01:01 +00:00
Fix OIDC auth cookie readiness
This commit is contained in:
@@ -1857,6 +1857,67 @@ ipcMain.handle("get-session-cookie", async (_event, name) => {
|
||||
}
|
||||
});
|
||||
|
||||
function cookieMatchesUrl(cookie, targetUrl) {
|
||||
if (!targetUrl) return true;
|
||||
|
||||
try {
|
||||
const targetHost = new URL(targetUrl).hostname;
|
||||
const cookieDomain = (cookie.domain || "").replace(/^\./, "");
|
||||
|
||||
return (
|
||||
cookieDomain === targetHost ||
|
||||
targetHost.endsWith(`.${cookieDomain}`) ||
|
||||
(!cookieDomain && targetHost === "localhost")
|
||||
);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle(
|
||||
"wait-session-cookie",
|
||||
async (_event, name, targetUrl, previousValue, timeoutMs = 5000) => {
|
||||
const ses = mainWindow?.webContents?.session;
|
||||
if (!ses) return { success: false, error: "No Electron session" };
|
||||
|
||||
const existingCookies = await ses.cookies.get({
|
||||
name,
|
||||
...(targetUrl ? { url: targetUrl } : {}),
|
||||
});
|
||||
const existingCookie = existingCookies.find((cookie) =>
|
||||
cookieMatchesUrl(cookie, targetUrl),
|
||||
);
|
||||
if (existingCookie?.value && existingCookie.value !== previousValue) {
|
||||
return { success: true, value: existingCookie.value };
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
ses.cookies.off("changed", onCookieChanged);
|
||||
resolve({ success: false, error: "Timed out waiting for cookie" });
|
||||
}, timeoutMs);
|
||||
|
||||
function onCookieChanged(_event, cookie, _cause, removed) {
|
||||
if (
|
||||
removed ||
|
||||
cookie.name !== name ||
|
||||
!cookie.value ||
|
||||
cookie.value === previousValue ||
|
||||
!cookieMatchesUrl(cookie, targetUrl)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
ses.cookies.off("changed", onCookieChanged);
|
||||
resolve({ success: true, value: cookie.value });
|
||||
}
|
||||
|
||||
ses.cookies.on("changed", onCookieChanged);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle("clear-session-cookies", async () => {
|
||||
try {
|
||||
const ses = mainWindow?.webContents?.session;
|
||||
|
||||
@@ -33,6 +33,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
ipcRenderer.invoke("start-c2s-autostart-tunnels"),
|
||||
|
||||
clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"),
|
||||
getSessionCookie: (name) => ipcRenderer.invoke("get-session-cookie", name),
|
||||
waitForSessionCookie: (name, targetUrl, previousValue, timeoutMs) =>
|
||||
ipcRenderer.invoke(
|
||||
"wait-session-cookie",
|
||||
name,
|
||||
targetUrl,
|
||||
previousValue,
|
||||
timeoutMs,
|
||||
),
|
||||
|
||||
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||
});
|
||||
|
||||
Vendored
+8
@@ -61,6 +61,14 @@ export interface ElectronAPI {
|
||||
started: number;
|
||||
errors: string[];
|
||||
}>;
|
||||
clearSessionCookies: () => Promise<void>;
|
||||
getSessionCookie: (name: string) => Promise<string | null>;
|
||||
waitForSessionCookie: (
|
||||
name: string,
|
||||
targetUrl?: string,
|
||||
previousValue?: string | null,
|
||||
timeoutMs?: number,
|
||||
) => Promise<{ success: boolean; value?: string; error?: string }>;
|
||||
|
||||
showSaveDialog: (options: DialogOptions) => Promise<DialogResult>;
|
||||
showOpenDialog: (options: DialogOptions) => Promise<DialogResult>;
|
||||
|
||||
@@ -139,25 +139,18 @@ export function Auth({
|
||||
const [dbConnectionFailed, setDbConnectionFailed] = useState(false);
|
||||
const [dbHealthChecking, setDbHealthChecking] = useState(false);
|
||||
|
||||
const handleElectronAuthSuccess = useCallback(async () => {
|
||||
const handleElectronAuthSuccess = useCallback(async (previousJwt: string | null) => {
|
||||
try {
|
||||
let retries = 5;
|
||||
let meRes = null;
|
||||
while (retries-- > 0) {
|
||||
try {
|
||||
meRes = await getUserInfo();
|
||||
break;
|
||||
} catch (err: any) {
|
||||
const isNoServer =
|
||||
err?.code === "NO_SERVER_CONFIGURED" ||
|
||||
err?.message?.includes("no-server-configured");
|
||||
if (isNoServer && retries > 0) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const cookieReady = await window.electronAPI?.waitForSessionCookie?.(
|
||||
"jwt",
|
||||
currentServerUrl,
|
||||
previousJwt,
|
||||
5000,
|
||||
);
|
||||
if (cookieReady && !cookieReady.success) {
|
||||
throw new Error(cookieReady.error || "Authentication cookie not ready");
|
||||
}
|
||||
const meRes = await getUserInfo();
|
||||
if (!meRes) throw new Error("Failed to get user info");
|
||||
setInternalLoggedIn(true);
|
||||
setLoggedIn(true);
|
||||
@@ -181,6 +174,7 @@ export function Auth({
|
||||
setUserId,
|
||||
t,
|
||||
setInternalLoggedIn,
|
||||
currentServerUrl,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -655,27 +649,32 @@ export function Auth({
|
||||
if (success) {
|
||||
setOidcLoading(true);
|
||||
|
||||
if (isInElectronWebView()) {
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "AUTH_SUCCESS",
|
||||
source: "oidc_callback",
|
||||
platform: "desktop",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
"*",
|
||||
);
|
||||
setWebviewAuthSuccess(true);
|
||||
setOidcLoading(false);
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
window.location.pathname,
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error("Error posting auth success message:", e);
|
||||
}
|
||||
}
|
||||
|
||||
getUserInfo()
|
||||
.then((meRes) => {
|
||||
if (isInElectronWebView()) {
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "AUTH_SUCCESS",
|
||||
source: "oidc_callback",
|
||||
platform: "desktop",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
"*",
|
||||
);
|
||||
setWebviewAuthSuccess(true);
|
||||
setOidcLoading(false);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error("Error posting auth success message:", e);
|
||||
}
|
||||
}
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
setLoggedIn(true);
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AlertCircle, Loader2, ArrowLeft, RefreshCw } from "lucide-react";
|
||||
|
||||
interface ElectronLoginFormProps {
|
||||
serverUrl: string;
|
||||
onAuthSuccess: () => void;
|
||||
onAuthSuccess: (previousJwt: string | null) => void | Promise<void>;
|
||||
onChangeServer: () => void;
|
||||
}
|
||||
|
||||
@@ -27,13 +27,29 @@ export function ElectronLoginForm({
|
||||
const isAuthenticatingRef = useRef(false);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const hasAuthenticatedRef = useRef(false);
|
||||
const [cookieSnapshotReady, setCookieSnapshotReady] = useState(false);
|
||||
const [currentUrl, setCurrentUrl] = useState(serverUrl);
|
||||
const hasLoadedOnce = useRef(false);
|
||||
const onAuthSuccessRef = useRef(onAuthSuccess);
|
||||
const initialJwtRef = useRef<string | null | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
onAuthSuccessRef.current = onAuthSuccess;
|
||||
}, [onAuthSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI
|
||||
?.getSessionCookie?.("jwt")
|
||||
.then((value) => {
|
||||
initialJwtRef.current = value;
|
||||
})
|
||||
.catch(() => {
|
||||
initialJwtRef.current = null;
|
||||
})
|
||||
.finally(() => {
|
||||
setCookieSnapshotReady(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAuthSuccess = useCallback(async () => {
|
||||
if (hasAuthenticatedRef.current || isAuthenticatingRef.current) return;
|
||||
hasAuthenticatedRef.current = true;
|
||||
@@ -41,7 +57,7 @@ export function ElectronLoginForm({
|
||||
setIsAuthenticating(true);
|
||||
|
||||
try {
|
||||
onAuthSuccessRef.current();
|
||||
await onAuthSuccessRef.current(initialJwtRef.current ?? null);
|
||||
} catch (_err) {
|
||||
setError(t("errors.authTokenSaveFailed"));
|
||||
isAuthenticatingRef.current = false;
|
||||
@@ -186,7 +202,7 @@ export function ElectronLoginForm({
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={serverUrl}
|
||||
src={cookieSnapshotReady ? serverUrl : "about:blank"}
|
||||
className="w-full h-full border-0"
|
||||
title="Server Authentication"
|
||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-storage-access-by-user-activation allow-top-navigation allow-top-navigation-by-user-activation allow-modals allow-downloads"
|
||||
|
||||
@@ -577,43 +577,41 @@ export function Auth({
|
||||
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
|
||||
setTimeout(() => {
|
||||
getUserInfo()
|
||||
.then((meRes) => {
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
setUsername(meRes.username || null);
|
||||
setUserId(meRes.userId || null);
|
||||
setDbError(null);
|
||||
postAuthSuccessToWebView();
|
||||
if (isReactNativeWebView()) {
|
||||
postAuthSuccessToWebView();
|
||||
setMobileAuthSuccess(true);
|
||||
setOidcLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isReactNativeWebView()) {
|
||||
setMobileAuthSuccess(true);
|
||||
setOidcLoading(false);
|
||||
return;
|
||||
}
|
||||
getUserInfo()
|
||||
.then((meRes) => {
|
||||
setIsAdmin(!!meRes.is_admin);
|
||||
setUsername(meRes.username || null);
|
||||
setUserId(meRes.userId || null);
|
||||
setDbError(null);
|
||||
|
||||
setLoggedIn(true);
|
||||
onAuthSuccess({
|
||||
isAdmin: !!meRes.is_admin,
|
||||
username: meRes.username || null,
|
||||
userId: meRes.userId || null,
|
||||
});
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to get user info after OIDC callback:", err);
|
||||
setError(t("errors.failedUserInfo"));
|
||||
setInternalLoggedIn(false);
|
||||
setLoggedIn(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
})
|
||||
.finally(() => {
|
||||
setOidcLoading(false);
|
||||
setLoggedIn(true);
|
||||
onAuthSuccess({
|
||||
isAdmin: !!meRes.is_admin,
|
||||
username: meRes.username || null,
|
||||
userId: meRes.userId || null,
|
||||
});
|
||||
}, 200);
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to get user info after OIDC callback:", err);
|
||||
setError(t("errors.failedUserInfo"));
|
||||
setInternalLoggedIn(false);
|
||||
setLoggedIn(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
})
|
||||
.finally(() => {
|
||||
setOidcLoading(false);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user