A signed-out visitor requests a deep personalised URL — how do you send them to sign-in and back afterwards?
answer
- capture the path before redirecting
- one response sets the session and points onward
- allowlist the shape, never a denylist
- one leading slash, no host
- never capture the sign-in route
basics
~20 sCapture the requested path and query, redirect to the sign-in route carrying it, and after a successful sign-in redirect there only if it is a local relative path; otherwise fall back to a default destination.
solid answer
~50 sWherever you discover there is no session — a pre-routing step or the route's own read — you already know the URL that was wanted, so capture its path and query before redirecting. Carry it either as a parameter on the sign-in URL or in a short-lived cookie; the parameter is simplest and survives multiple tabs, the cookie keeps it out of logs and referrers. After the credentials check succeeds, **validate before using it**: accept only a relative path beginning with a single `/` and no host, reject anything absolute or protocol-relative, and fall back to a default when it fails — otherwise you have built an open redirect that launders your domain's trust. Exclude the sign-in route itself from capture or you create a loop. And remember a POST cannot be replayed through this: the visitor lands on a GET of the destination and has to submit again.
go deeper
The idea to hold: the app remembers where the visitor was heading, sends them to sign in, and then sends them on. Losing that destination is why people complain about landing on the home page.
Describe the mechanics — capture path and query, carry it as a parameter or short-lived cookie, set the session and redirect in one response — and know that the target must be checked before it is used.
Demonstrate the security instinct unprompted: open redirect, the allowlisted shape, the fallback destination, loop exclusion for the sign-in route, and the fact that a form submission cannot be replayed.
Treat it as one flow owned in one place, with a documented rule for valid destinations, bounded lifetime for a remembered target, and a plan for the signed-in-but-not-permitted case that is not another bounce to sign-in.
## The round trip 1. A request arrives for a personalised URL and the session read returns nothing. 2. The handler builds a redirect to the sign-in route, carrying the originally requested **path and query** as the intended destination. 3. The visitor authenticates; that write handler sets the session cookie on its response. 4. That same response redirects — to the captured destination if it passes validation, otherwise to a default landing route. 5. The browser follows the redirect with the new cookie attached, and the destination renders signed in. Step 3 and step 4 being the *same* response is what makes this smooth: one response both establishes the session and points at where to go. ## Carrying the destination | Carrier | Strengths | Costs | |---|---|---| | Query parameter on the sign-in URL | Visible and debuggable; works across tabs; survives a bookmarked sign-in link | Appears in logs, history and referrers; fully attacker-controllable | | Short-lived cookie set with the redirect | Out of the URL; harder to hand someone a crafted link | Two tabs signing in at once overwrite each other; needs its own expiry | | Entry in server-side pre-auth state | Not attacker-controlled at all | Requires state before the visitor is known; more moving parts | All three are attacker-influenceable except the third, so **validation is mandatory regardless of carrier**. Choosing a cookie is not a substitute for checking the value. ## Validating the target The rule is an allowlist of shapes, not a denylist of bad strings: - Accept a **relative path starting with exactly one `/`**, optionally with a query string. - Reject anything containing a scheme, and reject `//host`, which browsers treat as protocol-relative and resolve to another origin. - Normalise before deciding, and decide on the normalised value, so encoded variants cannot smuggle a host past the check. - Optionally narrow further to paths that match a known route, which also protects against sending people to dead URLs. - On any failure, use the default destination. Silently falling back is correct behaviour here, not a swallowed error. Skipping this is the classic **open redirect**: an attacker sends a link to your real sign-in page, and after a genuine sign-in your site forwards the visitor to a lookalike that asks them to "confirm" their password. ## Loops and lost work - **Loops.** Never capture the sign-in or sign-out routes as a destination, and make sure the sign-in route is not itself behind the session requirement. A missed exclusion produces an endless bounce that looks like a broken app. - **Lost work.** A redirect cannot replay a form submission. If the session expires between loading a form and submitting it, the submission is gone and the visitor returns to a GET of the page. Where that is expensive, preserve the entered values yourself — or keep the destination pointing at the form, not at the action that failed. - **Expired mid-flow.** The nicest version of this remembers the destination for a bounded time only; a stale destination from an hour ago is more confusing than the default. ## The response itself The redirect is aimed at one visitor and the sign-in result carries a session cookie, so neither should be storable by a shared cache. Mark them accordingly. This is also why route protection cannot live in output that a shared tier may replay: a stored "go and sign in" response served to someone who *is* signed in, or the reverse, is a confusing and sometimes dangerous outcome. ## Testing the flow This is a flow with more branches than it looks, and each one has produced a real incident somewhere: 1. A deep link while signed out lands on the intended page after signing in, with its query string intact. 2. A crafted absolute destination falls back to the default instead of leaving the origin. 3. A protocol-relative destination beginning with two slashes falls back too. 4. Hitting the sign-in route directly while already signed in does something sensible rather than bouncing. 5. A destination captured a long time ago is discarded rather than honoured. 6. A signed-in visitor who lacks permission for the destination gets a refusal, not another trip to sign-in. ## Common mistakes - Sending everyone to the home page after sign-in, which quietly breaks every deep link, notification and email. - Validating with a substring check like "starts with our domain name", which `https://ourdomain.evil.example` passes. - Preserving the path but dropping the query string, so filters and pagination are lost. - Forgetting that the destination may itself now be forbidden — a signed-in visitor without the right entitlement still needs an answer other than another redirect to sign-in. - Capturing the sign-in route, then debugging an infinite loop.
- Why is a check that the destination starts with your own domain name not enough?Because a host is compared from the left with delimiters that matter. A value like `https://yourdomain.evil.example/x` starts with your domain name as a substring while resolving to an attacker's origin. Compare on the parsed shape instead: require a relative path with one leading slash and no scheme or authority, and reject everything else.
- What happens to a form submission that fails because the session expired?It is lost. A redirect turns the follow-up into a GET, so the body does not survive the round trip and the visitor sees the form again. Point the captured destination at the page that holds the form, and where the input is costly, preserve the entered values separately so the visitor is not retyping after signing in.
- Should the redirect to sign-in be produced before routing or inside the route?Both are used, and they answer different needs. A pre-routing step gives one consistent bounce for whole sections and runs before any rendering. A route-level read handles cases where the requirement depends on what the route is about to show. Many apps run both, with the route-level read as the one that must be correct.
saying these in an interview costs you the question
- Redirects everyone to the home page after sign-in
- Puts the captured destination in a Location header unvalidated
- Thinks a protocol-relative //host target is a local path
- Expects the failed POST body to be replayed after signing in
- Captures the sign-in route as the destination, creating a loop
- Lets a shared cache store the redirect meant for one visitor