fix: redirect to original page after re-authentication (#1959)

* fix: redirect to original page after re-authentication

When a session expires, the current URL is now preserved as a query
parameter on the login page. After successful login (including MFA
flows), the user is redirected back to their original page instead of
always landing on /home.

* secure

---------

Co-authored-by: Julien Fontanet <julien.fontanet@isonoe.net>
This commit is contained in:
Philip Okugbe
2026-02-21 00:02:23 +00:00
committed by GitHub
parent d6472f0876
commit 53132acb0a
8 changed files with 42 additions and 20 deletions
@@ -7,7 +7,7 @@ import { notifications } from "@mantine/notifications";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IAuthProvider } from "@/ee/security/types/security.types"; import { IAuthProvider } from "@/ee/security/types/security.types";
import APP_ROUTE from "@/lib/app-route"; import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
import { ldapLogin } from "@/ee/security/services/ldap-auth-service"; import { ldapLogin } from "@/ee/security/services/ldap-auth-service";
const formSchema = z.object({ const formSchema = z.object({
@@ -59,13 +59,13 @@ export function LdapLoginModal({
// Handle MFA like the regular login // Handle MFA like the regular login
if (response?.userHasMfa) { if (response?.userHasMfa) {
onClose(); onClose();
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE); navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + window.location.search);
} else if (response?.requiresMfaSetup) { } else if (response?.requiresMfaSetup) {
onClose(); onClose();
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED); navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + window.location.search);
} else { } else {
onClose(); onClose();
navigate(APP_ROUTE.HOME); navigate(getPostLoginRedirect());
} }
} catch (err: any) { } catch (err: any) {
setIsLoading(false); setIsLoading(false);
@@ -18,7 +18,7 @@ import { useNavigate } from "react-router-dom";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import classes from "./mfa-challenge.module.css"; import classes from "./mfa-challenge.module.css";
import { verifyMfa } from "@/ee/mfa"; import { verifyMfa } from "@/ee/mfa";
import APP_ROUTE from "@/lib/app-route"; import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as z from "zod"; import * as z from "zod";
import { MfaBackupCodeInput } from "./mfa-backup-code-input"; import { MfaBackupCodeInput } from "./mfa-backup-code-input";
@@ -53,7 +53,7 @@ export function MfaChallenge() {
setIsLoading(true); setIsLoading(true);
try { try {
await verifyMfa(values.code); await verifyMfa(values.code);
navigate(APP_ROUTE.HOME); navigate(getPostLoginRedirect());
} catch (error: any) { } catch (error: any) {
setIsLoading(false); setIsLoading(false);
notifications.show({ notifications.show({
@@ -3,7 +3,7 @@ import { Container, Paper, Title, Text, Alert, Stack } from "@mantine/core";
import { IconAlertCircle } from "@tabler/icons-react"; import { IconAlertCircle } from "@tabler/icons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MfaSetupModal } from "@/ee/mfa"; import { MfaSetupModal } from "@/ee/mfa";
import APP_ROUTE from "@/lib/app-route.ts"; import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route.ts";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function MfaSetupRequired() { export default function MfaSetupRequired() {
@@ -11,7 +11,7 @@ export default function MfaSetupRequired() {
const navigate = useNavigate(); const navigate = useNavigate();
const handleSetupComplete = () => { const handleSetupComplete = () => {
navigate(APP_ROUTE.HOME); navigate(getPostLoginRedirect());
}; };
return ( return (
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import APP_ROUTE from "@/lib/app-route"; import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
import { validateMfaAccess } from "@/ee/mfa"; import { validateMfaAccess } from "@/ee/mfa";
export function useMfaPageProtection() { export function useMfaPageProtection() {
@@ -13,8 +13,10 @@ export function useMfaPageProtection() {
const checkAccess = async () => { const checkAccess = async () => {
const result = await validateMfaAccess(); const result = await validateMfaAccess();
const search = location.search;
if (!result.valid) { if (!result.valid) {
navigate(APP_ROUTE.AUTH.LOGIN); navigate(APP_ROUTE.AUTH.LOGIN + search);
return; return;
} }
@@ -26,17 +28,17 @@ export function useMfaPageProtection() {
if (result.requiresMfaSetup && !isOnSetupPage) { if (result.requiresMfaSetup && !isOnSetupPage) {
// User needs to set up MFA but is on challenge page // User needs to set up MFA but is on challenge page
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED); navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + search);
} else if ( } else if (
!result.requiresMfaSetup && !result.requiresMfaSetup &&
result.userHasMfa && result.userHasMfa &&
!isOnChallengePage !isOnChallengePage
) { ) {
// User has MFA and should be on challenge page // User has MFA and should be on challenge page
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE); navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + search);
} else if (!result.isTransferToken) { } else if (!result.isTransferToken) {
// User has a regular auth token, shouldn't be on MFA pages // User has a regular auth token, shouldn't be on MFA pages
navigate(APP_ROUTE.HOME); navigate(getPostLoginRedirect());
} else { } else {
setIsValid(true); setIsValid(true);
} }
@@ -23,7 +23,7 @@ import {
acceptInvitation, acceptInvitation,
createWorkspace, createWorkspace,
} from "@/features/workspace/services/workspace-service.ts"; } from "@/features/workspace/services/workspace-service.ts";
import APP_ROUTE from "@/lib/app-route.ts"; import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route.ts";
import { RESET } from "jotai/utils"; import { RESET } from "jotai/utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { isCloud } from "@/lib/config.ts"; import { isCloud } from "@/lib/config.ts";
@@ -44,11 +44,11 @@ export default function useAuth() {
// Check if MFA is required // Check if MFA is required
if (response?.userHasMfa) { if (response?.userHasMfa) {
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE); navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + window.location.search);
} else if (response?.requiresMfaSetup) { } else if (response?.requiresMfaSetup) {
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED); navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + window.location.search);
} else { } else {
navigate(APP_ROUTE.HOME); navigate(getPostLoginRedirect());
} }
} catch (err) { } catch (err) {
setIsLoading(false); setIsLoading(false);
@@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import useCurrentUser from "@/features/user/hooks/use-current-user.ts"; import useCurrentUser from "@/features/user/hooks/use-current-user.ts";
import APP_ROUTE from "@/lib/app-route.ts"; import { getPostLoginRedirect } from "@/lib/app-route.ts";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export function useRedirectIfAuthenticated() { export function useRedirectIfAuthenticated() {
@@ -9,7 +9,7 @@ export function useRedirectIfAuthenticated() {
useEffect(() => { useEffect(() => {
if (data && data?.user) { if (data && data?.user) {
navigate(APP_ROUTE.HOME); navigate(getPostLoginRedirect());
} }
}, [isLoading, data]); }, [isLoading, data]);
} }
+5 -1
View File
@@ -68,10 +68,14 @@ function redirectToLogin() {
APP_ROUTE.AUTH.SIGNUP, APP_ROUTE.AUTH.SIGNUP,
APP_ROUTE.AUTH.FORGOT_PASSWORD, APP_ROUTE.AUTH.FORGOT_PASSWORD,
APP_ROUTE.AUTH.PASSWORD_RESET, APP_ROUTE.AUTH.PASSWORD_RESET,
APP_ROUTE.AUTH.MFA_CHALLENGE,
APP_ROUTE.AUTH.MFA_SETUP_REQUIRED,
"/invites", "/invites",
]; ];
if (!exemptPaths.some((path) => window.location.pathname.startsWith(path))) { if (!exemptPaths.some((path) => window.location.pathname.startsWith(path))) {
window.location.href = APP_ROUTE.AUTH.LOGIN; const redirectTo = window.location.pathname;
const params = new URLSearchParams({ redirect: redirectTo });
window.location.href = `${APP_ROUTE.AUTH.LOGIN}?${params.toString()}`;
} }
} }
+16
View File
@@ -29,4 +29,20 @@ const APP_ROUTE = {
}, },
}; };
export function getPostLoginRedirect(): string {
const params = new URLSearchParams(window.location.search);
const redirect = params.get("redirect");
if (redirect) {
try {
const resolved = new URL(redirect, window.location.origin);
if (resolved.origin === window.location.origin) {
return resolved.pathname + resolved.search + resolved.hash;
}
} catch {
// malformed URL, fall through to default
}
}
return APP_ROUTE.HOME;
}
export default APP_ROUTE; export default APP_ROUTE;