If the edge redirects (login, onboarding), deep links must survive the detour. Resist adding a second cookie for it — if the flow is OAuth-shaped, the
parameter already round-trips through the provider verbatim and is already authenticated by the CSRF check (state pinned in a cookie, compared timing-safe at the callback). Ride along:
state = base64url(JSON { nonce, returnTo })
. One value, one cookie, and an attacker can't swap the destination without breaking the comparison.
Wherever a returnTo enters (gate query, login page, callback's decoded state), validate it as a
same-origin relative path: starts with
, not
(protocol-relative is an absolute URL in disguise), and not an API path. Anything else falls back to
. Decoding must be total — providers send callbacks with state you never minted; junk reads as "no returnTo", never a throw.