Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions frontend/src/lib/hooks/redirect-uri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
type IuseRedirectUri = {
url?: URL;
valid: boolean;
trusted: boolean;
allowedProto: boolean;
httpsDowngrade: boolean;
};

export const useRedirectUri = (
redirect_uri: string | null,
cookieDomain: string,
): IuseRedirectUri => {
let isValid = false;
let isTrusted = false;
let isAllowedProto = false;
let isHttpsDowngrade = false;

if (!redirect_uri) {
return {
valid: false,
trusted: false,
allowedProto: false,
httpsDowngrade: false,
};
}

let url: URL;

try {
url = new URL(redirect_uri);
} catch {
return {
valid: false,
trusted: false,
allowedProto: false,
httpsDowngrade: false,
};
}

isValid = true;

if (
url.hostname == cookieDomain ||
url.hostname.endsWith(`.${cookieDomain}`)
) {
isTrusted = true;
}

if (url.protocol == "http:" || url.protocol == "https:") {
isAllowedProto = true;
}

if (window.location.protocol == "https:" && url.protocol == "http:") {
isHttpsDowngrade = true;
}

return {
url,
valid: isValid,
trusted: isTrusted,
allowedProto: isAllowedProto,
httpsDowngrade: isHttpsDowngrade,
};
};
9 changes: 0 additions & 9 deletions frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,6 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

export const isValidUrl = (url: string) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};

export const capitalize = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
90 changes: 46 additions & 44 deletions frontend/src/pages/continue-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
} from "@/components/ui/card";
import { useAppContext } from "@/context/app-context";
import { useUserContext } from "@/context/user-context";
import { isValidUrl } from "@/lib/utils";
import { Trans, useTranslation } from "react-i18next";
import { Navigate, useLocation, useNavigate } from "react-router";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRedirectUri } from "@/lib/hooks/redirect-uri";

export const ContinuePage = () => {
const { cookieDomain, disableUiWarnings } = useAppContext();
Expand All @@ -20,48 +20,35 @@ export const ContinuePage = () => {
const { t } = useTranslation();
const navigate = useNavigate();

const [loading, setLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [showRedirectButton, setShowRedirectButton] = useState(false);
const hasRedirected = useRef(false);

const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri");

const isValidRedirectUri =
redirectUri !== null ? isValidUrl(redirectUri) : false;
const redirectUriObj = isValidRedirectUri
? new URL(redirectUri as string)
: null;
const isTrustedRedirectUri =
redirectUriObj !== null
? redirectUriObj.hostname === cookieDomain ||
redirectUriObj.hostname.endsWith(`.${cookieDomain}`)
: false;
const isAllowedRedirectProto =
redirectUriObj !== null
? redirectUriObj.protocol === "https:" ||
redirectUriObj.protocol === "http:"
: false;
const isHttpsDowngrade =
redirectUriObj !== null
? redirectUriObj.protocol === "http:" &&
window.location.protocol === "https:"
: false;

const handleRedirect = () => {
setLoading(true);
window.location.assign(redirectUriObj!.toString());
};
const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
redirectUri,
cookieDomain,
);

const handleRedirect = useCallback(() => {
hasRedirected.current = true;
setIsLoading(true);
window.location.assign(url!);
}, [url]);

useEffect(() => {
if (!isLoggedIn) {
return;
}

if (hasRedirected.current) {
return;
}

if (
(!isValidRedirectUri ||
!isAllowedRedirectProto ||
!isTrustedRedirectUri ||
isHttpsDowngrade) &&
(!valid || !allowedProto || !trusted || httpsDowngrade) &&
!disableUiWarnings
) {
return;
Expand All @@ -72,30 +59,41 @@ export const ContinuePage = () => {
}, 100);

const reveal = setTimeout(() => {
setLoading(false);
setIsLoading(false);
setShowRedirectButton(true);
}, 5000);

return () => {
clearTimeout(auto);
clearTimeout(reveal);
};
});
}, [
isLoggedIn,
hasRedirected,
valid,
allowedProto,
trusted,
httpsDowngrade,
disableUiWarnings,
setIsLoading,
handleRedirect,
setShowRedirectButton,
]);

if (!isLoggedIn) {
return (
<Navigate
to={`/login?redirect_uri=${encodeURIComponent(redirectUri || "")}`}
to={`/login${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
replace
/>
);
}
Comment thread
steveiliop56 marked this conversation as resolved.

if (!isValidRedirectUri || !isAllowedRedirectProto) {
if (!valid || !allowedProto) {
return <Navigate to="/logout" replace />;
}

if (!isTrustedRedirectUri && !disableUiWarnings) {
if (!trusted && !disableUiWarnings) {
return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<CardHeader>
Expand All @@ -115,16 +113,16 @@ export const ContinuePage = () => {
</CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2">
<Button
onClick={handleRedirect}
loading={loading}
onClick={() => handleRedirect()}
loading={isLoading}
variant="destructive"
>
{t("continueTitle")}
</Button>
<Button
onClick={() => navigate("/logout")}
variant="outline"
disabled={loading}
disabled={isLoading}
>
{t("cancelTitle")}
</Button>
Expand All @@ -133,7 +131,7 @@ export const ContinuePage = () => {
);
}

if (isHttpsDowngrade && !disableUiWarnings) {
if (httpsDowngrade && !disableUiWarnings) {
return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<CardHeader>
Expand All @@ -151,13 +149,17 @@ export const ContinuePage = () => {
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2">
<Button onClick={handleRedirect} loading={loading} variant="warning">
<Button
onClick={() => handleRedirect()}
loading={isLoading}
variant="warning"
>
{t("continueTitle")}
</Button>
<Button
onClick={() => navigate("/logout")}
variant="outline"
disabled={loading}
disabled={isLoading}
>
{t("cancelTitle")}
</Button>
Expand All @@ -176,7 +178,7 @@ export const ContinuePage = () => {
</CardHeader>
{showRedirectButton && (
<CardFooter className="flex flex-col items-stretch">
<Button onClick={handleRedirect}>
<Button onClick={() => handleRedirect()}>
{t("continueRedirectManually")}
</Button>
</CardFooter>
Expand Down
Loading