From 78cbeee863e987831633ba8a61607a7a4548c56c Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Sat, 11 Jan 2025 22:06:14 +0100 Subject: [PATCH] Redirect to original page after login when returnPath is set --- .../account-management/WebApp/routes/login/index.tsx | 10 +++++++++- .../WebApp/routes/login/verify.tsx | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/application/account-management/WebApp/routes/login/index.tsx b/application/account-management/WebApp/routes/login/index.tsx index e9900e059..2ccdc0eac 100644 --- a/application/account-management/WebApp/routes/login/index.tsx +++ b/application/account-management/WebApp/routes/login/index.tsx @@ -18,6 +18,13 @@ import { loggedInPath, signUpPath } from "@repo/infrastructure/auth/constants"; import { useIsAuthenticated } from "@repo/infrastructure/auth/hooks"; export const Route = createFileRoute("/login/")({ + validateSearch: (search) => { + const returnPath = search.returnPath as string | undefined; + // Only allow paths starting with / to prevent open redirect attacks to external domains + return { + returnPath: returnPath?.startsWith("/") ? returnPath : undefined + }; + }, component: function LoginRoute() { const isAuthenticated = useIsAuthenticated(); @@ -40,6 +47,7 @@ export const Route = createFileRoute("/login/")({ export function LoginForm() { const [email, setEmail] = useState(""); + const { returnPath } = Route.useSearch(); const [{ success, errors, data, title, message }, action, isPending] = useActionState( api.actionPost("/api/account-management/authentication/login/start"), @@ -55,7 +63,7 @@ export function LoginForm() { expireAt: new Date(Date.now() + validForSeconds * 1000) }); - return ; + return ; } return ( diff --git a/application/account-management/WebApp/routes/login/verify.tsx b/application/account-management/WebApp/routes/login/verify.tsx index 129671edc..2b6bc34b7 100644 --- a/application/account-management/WebApp/routes/login/verify.tsx +++ b/application/account-management/WebApp/routes/login/verify.tsx @@ -19,6 +19,13 @@ import { useActionState, useEffect } from "react"; import { useIsAuthenticated } from "@repo/infrastructure/auth/hooks"; export const Route = createFileRoute("/login/verify")({ + validateSearch: (search) => { + const returnPath = search.returnPath as string | undefined; + // Only allow paths starting with / to prevent open redirect attacks to external domains + return { + returnPath: returnPath?.startsWith("/") ? returnPath : undefined + }; + }, component: function LoginVerifyRoute() { const isAuthenticated = useIsAuthenticated(); @@ -42,6 +49,7 @@ export const Route = createFileRoute("/login/verify")({ export function CompleteLoginForm() { const { email, loginId, expireAt } = getLoginState(); const { expiresInString, isExpired } = useExpirationTimeout(expireAt); + const { returnPath } = Route.useSearch(); const [{ success, title, message, errors }, action] = useActionState( api.actionPost("/api/account-management/authentication/login/{id}/complete"), @@ -66,9 +74,9 @@ export function CompleteLoginForm() { useEffect(() => { if (success) { - window.location.href = loggedInPath; + window.location.href = returnPath || loggedInPath; } - }, [success]); + }, [success, returnPath]); useEffect(() => { if (isExpired) {