From 5088d747f74b9c0cce735f49fe65d6649cb2d357 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:37:41 +0000 Subject: [PATCH 1/2] Fix team invite redirect being lost after email verification - Add VerificationModal to team invite page so users can verify email without losing their place in the invite flow - Store post_auth_redirect in sessionStorage before OAuth redirects (GitHub/Google) so the invite URL survives the round-trip - After email verification (modal or link), redirect to the stored URL instead of always going to / - Read post_auth_redirect in OAuth callback to navigate to the correct page after authentication Co-Authored-By: tony@opensecret.cloud --- frontend/src/components/VerificationModal.tsx | 9 +++++++++ frontend/src/routes/auth.$provider.callback.tsx | 5 +++++ frontend/src/routes/login.tsx | 6 ++++++ frontend/src/routes/signup.tsx | 6 ++++++ frontend/src/routes/team.invite.$inviteId.tsx | 4 ++++ frontend/src/routes/verify.$code.tsx | 10 +++++++++- 6 files changed, 39 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/VerificationModal.tsx b/frontend/src/components/VerificationModal.tsx index 247f8c4a..14c639de 100644 --- a/frontend/src/components/VerificationModal.tsx +++ b/frontend/src/components/VerificationModal.tsx @@ -7,6 +7,7 @@ import { DialogTitle } from "@/components/ui/dialog"; import { useOpenSecret } from "@opensecret/react"; +import { useNavigate } from "@tanstack/react-router"; import { useState, useEffect } from "react"; import { Loader2, CheckCircle, LogOut } from "lucide-react"; import { Input } from "./ui/input"; @@ -15,6 +16,7 @@ import { AlertDestructive } from "./AlertDestructive"; export function VerificationModal() { const os = useOpenSecret(); + const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(() => { if (!os.auth.user) return false; // Skip email verification for guest users and in local development @@ -78,6 +80,13 @@ export function VerificationModal() { setIsVerifying(true); await os.verifyEmail(verificationCode); await os.refetchUser(); + + // Check for a pending redirect (e.g. team invite page) after email verification + const pendingRedirect = sessionStorage.getItem("post_auth_redirect"); + if (pendingRedirect) { + sessionStorage.removeItem("post_auth_redirect"); + navigate({ to: pendingRedirect }); + } } catch (err) { if (err instanceof Error) { setError(err.message); diff --git a/frontend/src/routes/auth.$provider.callback.tsx b/frontend/src/routes/auth.$provider.callback.tsx index 4687e8e1..f2c0ad65 100644 --- a/frontend/src/routes/auth.$provider.callback.tsx +++ b/frontend/src/routes/auth.$provider.callback.tsx @@ -62,12 +62,17 @@ function OAuthCallback() { const selectedPlan = sessionStorage.getItem("selected_plan"); sessionStorage.removeItem("selected_plan"); + const postAuthRedirect = sessionStorage.getItem("post_auth_redirect"); + sessionStorage.removeItem("post_auth_redirect"); + setTimeout(() => { if (selectedPlan) { navigate({ to: "/pricing", search: { selected_plan: selectedPlan } }); + } else if (postAuthRedirect) { + navigate({ to: postAuthRedirect }); } else { navigate({ to: "/" }); } diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index 47822ae7..f426ef14 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -139,6 +139,9 @@ function LoginPage() { if (code) { sessionStorage.setItem("redeem_code", code); } + if (next) { + sessionStorage.setItem("post_auth_redirect", next); + } window.location.href = auth_url; } } catch (error) { @@ -181,6 +184,9 @@ function LoginPage() { if (code) { sessionStorage.setItem("redeem_code", code); } + if (next) { + sessionStorage.setItem("post_auth_redirect", next); + } window.location.href = auth_url; } } catch (error) { diff --git a/frontend/src/routes/signup.tsx b/frontend/src/routes/signup.tsx index 7fc581cb..6e574f6f 100644 --- a/frontend/src/routes/signup.tsx +++ b/frontend/src/routes/signup.tsx @@ -150,6 +150,9 @@ function SignupPage() { if (code) { sessionStorage.setItem("redeem_code", code); } + if (next) { + sessionStorage.setItem("post_auth_redirect", next); + } window.location.href = auth_url; } } catch (error) { @@ -192,6 +195,9 @@ function SignupPage() { if (code) { sessionStorage.setItem("redeem_code", code); } + if (next) { + sessionStorage.setItem("post_auth_redirect", next); + } window.location.href = auth_url; } } catch (error) { diff --git a/frontend/src/routes/team.invite.$inviteId.tsx b/frontend/src/routes/team.invite.$inviteId.tsx index 9b7e2924..3d552277 100644 --- a/frontend/src/routes/team.invite.$inviteId.tsx +++ b/frontend/src/routes/team.invite.$inviteId.tsx @@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, UserPlus, AlertCircle, CheckCircle, XCircle } from "lucide-react"; import type { CheckInviteResponse } from "@/types/team"; +import { VerificationModal } from "@/components/VerificationModal"; export const Route = createFileRoute("/team/invite/$inviteId")({ component: TeamInviteAcceptance @@ -44,6 +45,8 @@ function TeamInviteAcceptance() { // Redirect to signup if not authenticated useEffect(() => { if (!isLoggedIn && !checkingInvite) { + // Store the invite URL so it survives OAuth redirects and email verification + sessionStorage.setItem("post_auth_redirect", `/team/invite/${inviteId}`); navigate({ to: "/signup", search: { @@ -168,6 +171,7 @@ function TeamInviteAcceptance() { return ( <> +
diff --git a/frontend/src/routes/verify.$code.tsx b/frontend/src/routes/verify.$code.tsx index 4fa80663..20708274 100644 --- a/frontend/src/routes/verify.$code.tsx +++ b/frontend/src/routes/verify.$code.tsx @@ -24,7 +24,15 @@ function VerifyEmail() { // Do both refetch and navigation after a delay setTimeout(async () => { await refetchUser(); - navigate({ to: "/" }); + + // Check for a pending redirect (e.g. team invite page) + const pendingRedirect = sessionStorage.getItem("post_auth_redirect"); + if (pendingRedirect) { + sessionStorage.removeItem("post_auth_redirect"); + navigate({ to: pendingRedirect }); + } else { + navigate({ to: "/" }); + } }, 2000); } catch (err) { if (err instanceof Error) { From 128ea40578b571fc61c8490cf3eb36be6aefb321 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:23:51 +0000 Subject: [PATCH 2/2] Add internal-path validation for post_auth_redirect Validate that post_auth_redirect values start with '/' and not '//' to prevent open redirect attacks via user-controlled query params. Applied at both storage (login/signup) and consumption (callback, verification modal, verify link) points. Co-Authored-By: tony@opensecret.cloud --- frontend/src/components/VerificationModal.tsx | 4 ++-- frontend/src/routes/auth.$provider.callback.tsx | 8 ++++++-- frontend/src/routes/login.tsx | 4 ++-- frontend/src/routes/signup.tsx | 4 ++-- frontend/src/routes/verify.$code.tsx | 8 ++++++-- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/VerificationModal.tsx b/frontend/src/components/VerificationModal.tsx index 14c639de..27bfa353 100644 --- a/frontend/src/components/VerificationModal.tsx +++ b/frontend/src/components/VerificationModal.tsx @@ -83,8 +83,8 @@ export function VerificationModal() { // Check for a pending redirect (e.g. team invite page) after email verification const pendingRedirect = sessionStorage.getItem("post_auth_redirect"); - if (pendingRedirect) { - sessionStorage.removeItem("post_auth_redirect"); + sessionStorage.removeItem("post_auth_redirect"); + if (pendingRedirect && pendingRedirect.startsWith("/") && !pendingRedirect.startsWith("//")) { navigate({ to: pendingRedirect }); } } catch (err) { diff --git a/frontend/src/routes/auth.$provider.callback.tsx b/frontend/src/routes/auth.$provider.callback.tsx index f2c0ad65..f1332fa8 100644 --- a/frontend/src/routes/auth.$provider.callback.tsx +++ b/frontend/src/routes/auth.$provider.callback.tsx @@ -64,6 +64,10 @@ function OAuthCallback() { const postAuthRedirect = sessionStorage.getItem("post_auth_redirect"); sessionStorage.removeItem("post_auth_redirect"); + const safePostAuthRedirect = + postAuthRedirect && postAuthRedirect.startsWith("/") && !postAuthRedirect.startsWith("//") + ? postAuthRedirect + : null; setTimeout(() => { if (selectedPlan) { @@ -71,8 +75,8 @@ function OAuthCallback() { to: "/pricing", search: { selected_plan: selectedPlan } }); - } else if (postAuthRedirect) { - navigate({ to: postAuthRedirect }); + } else if (safePostAuthRedirect) { + navigate({ to: safePostAuthRedirect }); } else { navigate({ to: "/" }); } diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index f426ef14..c25f4cfd 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -139,7 +139,7 @@ function LoginPage() { if (code) { sessionStorage.setItem("redeem_code", code); } - if (next) { + if (next && next.startsWith("/") && !next.startsWith("//")) { sessionStorage.setItem("post_auth_redirect", next); } window.location.href = auth_url; @@ -184,7 +184,7 @@ function LoginPage() { if (code) { sessionStorage.setItem("redeem_code", code); } - if (next) { + if (next && next.startsWith("/") && !next.startsWith("//")) { sessionStorage.setItem("post_auth_redirect", next); } window.location.href = auth_url; diff --git a/frontend/src/routes/signup.tsx b/frontend/src/routes/signup.tsx index 6e574f6f..fe0f3087 100644 --- a/frontend/src/routes/signup.tsx +++ b/frontend/src/routes/signup.tsx @@ -150,7 +150,7 @@ function SignupPage() { if (code) { sessionStorage.setItem("redeem_code", code); } - if (next) { + if (next && next.startsWith("/") && !next.startsWith("//")) { sessionStorage.setItem("post_auth_redirect", next); } window.location.href = auth_url; @@ -195,7 +195,7 @@ function SignupPage() { if (code) { sessionStorage.setItem("redeem_code", code); } - if (next) { + if (next && next.startsWith("/") && !next.startsWith("//")) { sessionStorage.setItem("post_auth_redirect", next); } window.location.href = auth_url; diff --git a/frontend/src/routes/verify.$code.tsx b/frontend/src/routes/verify.$code.tsx index 20708274..ab681a06 100644 --- a/frontend/src/routes/verify.$code.tsx +++ b/frontend/src/routes/verify.$code.tsx @@ -27,8 +27,12 @@ function VerifyEmail() { // Check for a pending redirect (e.g. team invite page) const pendingRedirect = sessionStorage.getItem("post_auth_redirect"); - if (pendingRedirect) { - sessionStorage.removeItem("post_auth_redirect"); + sessionStorage.removeItem("post_auth_redirect"); + if ( + pendingRedirect && + pendingRedirect.startsWith("/") && + !pendingRedirect.startsWith("//") + ) { navigate({ to: pendingRedirect }); } else { navigate({ to: "/" });