diff --git a/cmd/root.go b/cmd/root.go
index 3ae7292..171e043 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -95,7 +95,6 @@ func init() {
{"generic-user-url", "", "Generic OAuth user info URL."},
{"generic-name", "Generic", "Generic OAuth provider name."},
{"generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider."},
- {"disable-continue", false, "Disable continue screen and redirect to app directly."},
{"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."},
{"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"},
{"session-expiry", 86400, "Session (cookie) expiration time in seconds."},
diff --git a/frontend/bun.lock b/frontend/bun.lock
index 12b197b..1f98f9c 100644
--- a/frontend/bun.lock
+++ b/frontend/bun.lock
@@ -14,7 +14,6 @@
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "dompurify": "^3.2.6",
"i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-resources-to-backend": "^1.2.1",
@@ -364,8 +363,6 @@
"@types/react-dom": ["@types/react-dom@19.1.8", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ=="],
- "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
-
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.41.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/type-utils": "8.41.0", "@typescript-eslint/utils": "8.41.0", "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw=="],
@@ -476,8 +473,6 @@
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
- "dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="],
-
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"electron-to-chromium": ["electron-to-chromium@1.5.151", "", {}, "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA=="],
diff --git a/frontend/package.json b/frontend/package.json
index 2161e05..3d3fc47 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -20,7 +20,6 @@
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "dompurify": "^3.2.6",
"i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-resources-to-backend": "^1.2.1",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 72b9238..0559b26 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -5,8 +5,8 @@ export const App = () => {
const { isLoggedIn } = useUserContext();
if (isLoggedIn) {
- return ;
+ return ;
}
- return ;
+ return ;
};
diff --git a/frontend/src/components/domain-warning/domain-warning.tsx b/frontend/src/components/domain-warning/domain-warning.tsx
new file mode 100644
index 0000000..4f83b23
--- /dev/null
+++ b/frontend/src/components/domain-warning/domain-warning.tsx
@@ -0,0 +1,56 @@
+import {
+ Card,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "../ui/card";
+import { Button } from "../ui/button";
+import { Trans, useTranslation } from "react-i18next";
+import { useLocation } from "react-router";
+
+interface Props {
+ onClick: () => void;
+ appUrl: string;
+ currentUrl: string;
+}
+
+export const DomainWarning = (props: Props) => {
+ const { onClick, appUrl, currentUrl } = props;
+ const { t } = useTranslation();
+ const { search } = useLocation();
+
+ const searchParams = new URLSearchParams(search);
+ const redirectUri = searchParams.get("redirect_uri");
+
+ return (
+
+
+ {t("domainWarningTitle")}
+
+ }}
+ />
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/layout/layout.tsx b/frontend/src/components/layout/layout.tsx
index 773185b..3461000 100644
--- a/frontend/src/components/layout/layout.tsx
+++ b/frontend/src/components/layout/layout.tsx
@@ -1,8 +1,10 @@
import { useAppContext } from "@/context/app-context";
import { LanguageSelector } from "../language/language";
import { Outlet } from "react-router";
+import { useCallback, useState } from "react";
+import { DomainWarning } from "../domain-warning/domain-warning";
-export const Layout = () => {
+const BaseLayout = ({ children }: { children: React.ReactNode }) => {
const { backgroundImage } = useAppContext();
return (
@@ -15,7 +17,38 @@ export const Layout = () => {
}}
>
-
+ {children}
);
};
+
+export const Layout = () => {
+ const { appUrl } = useAppContext();
+ const [ignoreDomainWarning, setIgnoreDomainWarning] = useState(() => {
+ return window.sessionStorage.getItem("ignoreDomainWarning") === "true";
+ });
+ const currentUrl = window.location.origin;
+
+ const handleIgnore = useCallback(() => {
+ window.sessionStorage.setItem("ignoreDomainWarning", "true");
+ setIgnoreDomainWarning(true);
+ }, [setIgnoreDomainWarning]);
+
+ if (!ignoreDomainWarning && appUrl !== currentUrl) {
+ return (
+
+ handleIgnore()}
+ />
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
index fbb5b27..4badcc1 100644
--- a/frontend/src/components/ui/button.tsx
+++ b/frontend/src/components/ui/button.tsx
@@ -22,7 +22,7 @@ const buttonVariants = cva(
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
warning:
- "bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40 dark:bg-amber-600",
+ "bg-amber-500 text-white shadow-xs hover:bg-amber-400 focus-visible:ring-amber-200/20 dark:focus-visible:ring-amber-400/40",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 0b1ee02..9701636 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -156,7 +156,7 @@ ul {
}
code {
- @apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold;
+ @apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
}
.lead {
diff --git a/frontend/src/lib/i18n/locales/en-US.json b/frontend/src/lib/i18n/locales/en-US.json
index 74e422f..b2dd900 100644
--- a/frontend/src/lib/i18n/locales/en-US.json
+++ b/frontend/src/lib/i18n/locales/en-US.json
@@ -14,14 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
+ "continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
- "continueInvalidRedirectTitle": "Invalid redirect",
- "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
+ "continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?",
- "continueTitle": "Continue",
- "continueSubtitle": "Click the button to continue to your app.",
+ "continueUntrustedRedirectTitle": "Untrusted redirect",
+ "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{rootDomain}}). Are you sure you want to continue?",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -44,8 +44,6 @@
"unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.",
"unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.",
"unauthorizedButton": "Try again",
- "untrustedRedirectTitle": "Untrusted redirect",
- "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?",
"cancelTitle": "Cancel",
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
@@ -53,5 +51,9 @@
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
- "invalidInput": "Invalid input"
+ "invalidInput": "Invalid input",
+ "domainWarningTitle": "Invalid Domain",
+ "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.",
+ "ignoreTitle": "Ignore",
+ "goToCorrectDomainTitle": "Go to correct domain"
}
\ No newline at end of file
diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json
index 74e422f..b2dd900 100644
--- a/frontend/src/lib/i18n/locales/en.json
+++ b/frontend/src/lib/i18n/locales/en.json
@@ -14,14 +14,14 @@
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
+ "continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
- "continueInvalidRedirectTitle": "Invalid redirect",
- "continueInvalidRedirectSubtitle": "The redirect URL is invalid",
+ "continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from https to http which is not secure. Are you sure you want to continue?",
- "continueTitle": "Continue",
- "continueSubtitle": "Click the button to continue to your app.",
+ "continueUntrustedRedirectTitle": "Untrusted redirect",
+ "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{rootDomain}}). Are you sure you want to continue?",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
@@ -44,8 +44,6 @@
"unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.",
"unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.",
"unauthorizedButton": "Try again",
- "untrustedRedirectTitle": "Untrusted redirect",
- "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?",
"cancelTitle": "Cancel",
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
@@ -53,5 +51,9 @@
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
- "invalidInput": "Invalid input"
+ "invalidInput": "Invalid input",
+ "domainWarningTitle": "Invalid Domain",
+ "domainWarningSubtitle": "This instance is configured to be accessed from {{appUrl}}, but {{currentUrl}} is being used. If you proceed, you may encounter issues with authentication.",
+ "ignoreTitle": "Ignore",
+ "goToCorrectDomainTitle": "Go to correct domain"
}
\ No newline at end of file
diff --git a/frontend/src/pages/continue-page.tsx b/frontend/src/pages/continue-page.tsx
index cc4d432..261be8b 100644
--- a/frontend/src/pages/continue-page.tsx
+++ b/frontend/src/pages/continue-page.tsx
@@ -11,60 +11,101 @@ 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 DOMPurify from "dompurify";
-import { useState } from "react";
+import { useEffect, useState } from "react";
export const ContinuePage = () => {
+ const { rootDomain } = useAppContext();
const { isLoggedIn } = useUserContext();
-
- if (!isLoggedIn) {
- return ;
- }
-
- const { domain, disableContinue } = useAppContext();
const { search } = useLocation();
- const [loading, setLoading] = useState(false);
-
- const searchParams = new URLSearchParams(search);
- const redirectURI = searchParams.get("redirect_uri");
-
- if (!redirectURI) {
- return ;
- }
-
- if (!isValidUrl(DOMPurify.sanitize(redirectURI))) {
- return ;
- }
-
- const handleRedirect = () => {
- setLoading(true);
- window.location.href = DOMPurify.sanitize(redirectURI);
- }
-
- if (disableContinue) {
- handleRedirect();
- }
-
const { t } = useTranslation();
const navigate = useNavigate();
- const url = new URL(redirectURI);
+ const [loading, setLoading] = useState(false);
+ const [showRedirectButton, setShowRedirectButton] = useState(false);
- if (!(url.hostname == domain) && !url.hostname.endsWith(`.${domain}`)) {
+ 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 === rootDomain ||
+ redirectUriObj.hostname.endsWith(`.${rootDomain}`)
+ : 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());
+ };
+
+ useEffect(() => {
+ if (
+ !isLoggedIn ||
+ !isValidRedirectUri ||
+ !isTrustedRedirectUri ||
+ !isAllowedRedirectProto ||
+ isHttpsDowngrade
+ ) {
+ return;
+ }
+
+ const auto = setTimeout(() => {
+ handleRedirect();
+ }, 100);
+
+ const reveal = setTimeout(() => {
+ setLoading(false);
+ setShowRedirectButton(true);
+ }, 1000);
+
+ return () => {
+ clearTimeout(auto);
+ clearTimeout(reveal);
+ };
+ }, []);
+
+ if (!isLoggedIn) {
return (
-
+
+ );
+ }
+
+ if (!isValidRedirectUri || !isAllowedRedirectProto) {
+ return ;
+ }
+
+ if (!isTrustedRedirectUri) {
+ return (
+
- {t("untrustedRedirectTitle")}
+ {t("continueUntrustedRedirectTitle")}
,
}}
- values={{ domain }}
+ values={{ rootDomain }}
/>
@@ -76,7 +117,11 @@ export const ContinuePage = () => {
>
{t("continueTitle")}
-