Meet Modi
Back to Blog
·6 min

The redirect that forgot where you were going

A user clicked a link straight to a class recordings page. Their session had expired. They logged back in and landed on the dashboard home instead, three clicks away from where they started.

By Meet Modi
AuthenticationRoutingUX

A user with an expired session clicked a deep link straight to a specific class's recordings page. The app bounced them to a separate authentication service to log back in. They logged in fine. Then they landed on the dashboard's root page, not the recordings page they'd clicked toward.

They had to navigate back to the class, click into recordings again, a support ticket's worth of friction for something that should have been invisible.

The bug: the redirect out remembered nothing. The auth service has no reason to know what page a user originally wanted, it only knows how to log someone in and send them back to wherever it's told.

Take 1: redirect out, hope for the best

function requireAuth() {
  const isAuthenticated = checkSession();
  if (!isAuthenticated) {
    window.location.href = `https://auth.example.com/login?returnTo=${encodeURIComponent('https://app.example.com/dashboard')}`;
  }
}

returnTo always points at the dashboard, because that's the one destination that's always valid to land on. It works. It just throws away wherever the user actually was, every single time, regardless of whether they came from the dashboard or three levels deep in a specific class.

The auth service isn't the problem here. It's doing exactly what it was told: log the user in, send them to returnTo. The information about the real destination was never passed to it, because I never captured it before redirecting away in the first place.

Take 2: stash the destination before you leave

Before redirecting to the auth service, I write the current path and query string to sessionStorage under a fixed key. sessionStorage survives the round trip to the auth service and back, since it's scoped to the browser tab, not to any single page load.

const RETURN_PATH_KEY = 'post-login-redirect';

function requireAuth() {
  const isAuthenticated = checkSession();
  if (!isAuthenticated) {
    const destination = window.location.pathname + window.location.search;
    sessionStorage.setItem(RETURN_PATH_KEY, destination);
    window.location.href = `https://auth.example.com/login?returnTo=${encodeURIComponent('https://app.example.com/dashboard')}`;
  }
}

Then, in the component that handles post-login onboarding, the first thing that runs after the app regains control reads that key back and resumes navigation to wherever the user actually intended to go.

function PostLoginHandler() {
  const router = useRouter();

  useEffect(() => {
    const destination = sessionStorage.getItem(RETURN_PATH_KEY);
    sessionStorage.removeItem(RETURN_PATH_KEY);
    router.replace(destination ?? '/dashboard');
  }, []);

  return <LoadingSpinner />;
}

The key gets removed immediately after reading, not after the navigation resolves. A stale value sitting in sessionStorage from an earlier abandoned login attempt would otherwise redirect a completely unrelated future login to the wrong place.

What I should have done first

I should have treated "where does the user end up after login" as a first-class part of the auth flow instead of an afterthought bolted onto returnTo. The dashboard-as-default worked fine for every test login I did myself, because I always started from the dashboard. The bug only shows up for deep links, and deep links are exactly how real users arrive, from a shared link, a bookmark, a notification. Testing only the path I personally take through my own app is how this kind of gap survives to production.

More Posts