Authentication · intermediate
Login works once then redirects forever: trace the session contract
Browser appears to authenticate successfully on the first request but is then bounced between the identity provider and the application indefinitely. The root cause is almost always a disagreement in the session contract: cookies that the server believes it set are not the cookies the middleware or callback handler observes on the next hop.
The symptoms
- •After entering credentials, the URL bar oscillates between the application's callback URL and its root or login page without ever settling on a 200 response.
- •The first successful login works in one tab but a hard refresh in the same tab re-enters the loop, indicating state is not surviving the redirect chain.
- •Network panel shows a repeating chain of 302 or 303 responses with no terminal 200, and the Location values point back and forth between two hosts.
- •The session cookie appears in DevTools Application → Cookies but the server still classifies the same request as anonymous in its logs.
- •The loop only manifests when the callback crosses schemes (http to https), ports, or subdomains, and disappears in a fresh private window until cookies accumulate.
- •Browser console emits warnings about SameSite, partitioned storage, or "cookie was blocked because it is not marked Secure" around the loop window.
Likely causes
- •Set-Cookie is issued with a Domain or Path attribute that does not cover the URL the callback returns on, so the browser drops the cookie on the very next hop.
- •SameSite=None is set without the Secure flag, causing the browser to withhold the cookie on the cross-site callback request per the contract described in the MDN Set-Cookie reference.
- •The callback redirect target uses a different origin (scheme, host, or port) from the issuer, so the cookie's scope does not apply on the return trip.
- •Auth middleware executes an "is authenticated" check before session parsing completes, so an IDP-issued session token is treated as anonymous on the callback route.
- •The session store (Redis, database, in-memory map) loses the session record between callback and the next request, while the browser still presents a valid-looking cookie.
- •A reverse proxy, CDN, or edge function strips, duplicates, or rewrites Set-Cookie attributes, so the downstream browser receives a cookie the application never intended.
- •Two cookies with the same name but different scopes are present after callback, and the server is reading the unscoped one while the browser is sending the scoped one.
First ten minutes
- 01Open the browser DevTools Network panel with "Preserve log" enabled and clear only the disabled cache option, then reproduce the loop without clearing cookies.
- 02In Application → Cookies, expand both the application origin and the identity provider origin and record name, value, domain, path, expires, SameSite, Secure, and HttpOnly for every cookie.
- 03Diff the host and scheme of each Location header against the Domain attribute of the cookie that was supposed to travel with it; mark any mismatch.
- 04Re-run the flow in a private window to isolate whether persisted state (stale cookies, stale sessions) is contributing to the loop.
- 05Search application logs around the loop's start time for the session identifier being created and then reported as "not found" or "anonymous" within the same second.
- 06If a proxy or CDN is in front, capture the wire-level Set-Cookie from the upstream response and from the downstream response and compare them line by line.
Evidence to collect
- •Complete Set-Cookie response header lines for each hop, including every attribute token, not just the name=value pair.
- •The Cookie request header sent on each redirect, especially the first request back to the application immediately after the IDP callback completes.
- •The full Location chain with HTTP status codes, including intermediate 3xx responses from any proxy or edge layer.
- •The session identifier as observed by the IDP, the session store, and any application-side signed token, and whether these three values agree.
- •Server logs keyed by the same session identifier covering at least two seconds before and after the loop begins, including any "session not found" lines.
- •Browser console messages about cookie blocking, SameSite, partitioned storage, or third-party cookie restrictions observed during the loop.
Where to look
- •Browser DevTools Network panel filtered by status 3xx with "Preserve log" enabled across the full flow including redirects.
- •Browser DevTools Application panel, Storage → Cookies, expanded per origin so scoped and host-only cookies are visible side by side.
- •Application server access logs and framework debug logs filtered to the session identifier or correlation ID emitted by the IDP.
- •Identity provider audit or sign-in logs for the same user and correlation ID, especially the callback accepted event and any token re-issuance.
- •Reverse proxy, load balancer, or CDN logs that record whether Set-Cookie was modified, stripped, or duplicated between origin and client.
- •Edge functions or auth middleware configuration that may rewrite Location, inject headers, or run a redirect before session parsing completes.
Diagnostic steps
- 01Reproduce the loop with the Network log preserved; identify the first 3xx whose Location target does not include the cookie that was just set, and note the cookie's Domain and Path.
- 02Read the Set-Cookie attributes per the MDN Set-Cookie reference and verify Domain, Path, Secure, HttpOnly, SameSite, and Expires against the URL that will receive the next request.
- 03Confirm the callback URL registered with the IDP matches byte-for-byte (scheme, host, port, path, trailing slash) the URL the application actually redirects to.
- 04Temporarily add an early return in the callback handler that logs the inbound Cookie request header; observe whether the expected cookie is present on the very first callback hit.
- 05Issue a non-redirected request from the same origin with the same cookie value and observe whether middleware accepts or rejects it, to separate cookie scope from session lookup.
- 06If a proxy is involved, compare headers from a request that bypasses the proxy against one that goes through it; diff the Set-Cookie and Cookie headers line by line using a read-only diff command.
- 07Replay the exact redirect chain with curl and a cookie jar against a non-production host, then inspect which hop first loses the cookie; do not run this against production credentials.
Common mistakes
- •Trusting that a Set-Cookie "looks right" without verifying the Domain attribute against the actual host the browser will send the cookie to on the return trip.
- •Treating "SameSite=Lax by default" as a guarantee when the redirect is cross-site, since Lax still withholds cookies on cross-site sub-requests such as the IDP callback.
- •Comparing session IDs between IDP and application without normalizing for re-issuance immediately after callback, then concluding the IDs disagree when they were never meant to match.
- •Clearing browser cookies as the first debugging step, which destroys the only evidence of which cookies actually persist into the loop window.
- •Conflating framework session cookies with IDP-issued cookies in logs, then stating "the cookie is set" without specifying which cookie on which origin.
- •Assuming the redirect loop is CSRF protection rejecting a missing token, when in fact a valid token is present but a mismatched cookie is preventing its acceptance.
Safe fixes
- •Align the cookie Domain attribute to a parent of both the issuer and callback hosts, or remove Domain entirely so the cookie is scoped to the exact issuer host.
- •When the callback crosses origins, set SameSite=None together with Secure and verify that the page serving the cookie is served over HTTPS end to end.
- •Register the exact callback URL (scheme, host, port, path, trailing slash) with the IDP, and reject requests whose redirect target differs at the edge before middleware runs.
- •Reorder auth middleware so cookie and session parsing precedes any "is authenticated" check on the callback route, and confirm with a temporary log line.
- •Configure the reverse proxy or CDN to pass through Set-Cookie headers unchanged, or document and verify any rewriting with a diff between upstream and downstream captures.
- •Use a session store with persistence that outlives at least one full callback round-trip, and verify with a short retention test rather than assuming in-memory storage survives a redirect.
Prove the fix
- 01Run the flow in a private window with the Network log preserved and observe a single 302 chain terminating in a 200, with no Location value pointing back to a previous hop.
- 02Replay the exact redirect chain with curl using --cookie-jar and --cookie against a test host (placeholders such as <idp-host> and <app-host>) and confirm the final status is 200, not 3xx.
- 03In DevTools, confirm that the cookie sent on the post-callback request matches the cookie set on the pre-callback response by name, domain, path, and SameSite.
- 04Grep server logs for the session identifier across at least one minute and confirm there is no "session not found" or "anonymous" classification between the callback and the landing page.
- 05Toggle only the previously failing attribute (for example, switch SameSite to None; Secure) and observe the loop resolves with no other code changes, ruling out coincidental fixes.
- 06Have a second engineer repeat the flow in their own browser against the same environment to rule out local cookie or extension contamination.
Prevention and next steps
- •Add an automated contract test that fetches the live Set-Cookie headers from the IDP callback and asserts they will apply on the application's callback origin per the MDN Set-Cookie attribute contract.
- •Lint cookie attribute combinations at startup: reject SameSite=None without Secure, reject Domain values that exclude the callback host, and warn on Path values narrower than the callback path.
- •Document the authenticated state machine (anonymous, callback-pending, authenticated) and instrument every transition with a structured log line and a metric so loops become visible before users report them.
- •Pin the callback URL in IDP configuration to a single canonical value and reject mismatched requests at the edge before they reach middleware, with a clear error code.
- •Add a synthetic monitoring flow that logs in periodically and asserts the redirect chain terminates within a fixed hop count, alerting on any cycle detected.
Safe commands and checks
curl -sSI -H 'User-Agent: <ua-string>' '<idp-host>/<login-path>' | tr -d '\r' | grep -i '^set-cookie:' # read-only; prints every Set-Cookie line and its attributes from the IDP login response.
curl -sSI -H 'Cookie: <session-name>=<opaque>' '<app-host>/<protected-path>' | head -n 20 # read-only; confirms whether the application accepts a cookie sent on a non-redirected request.
grep -nE 'session_id|<session-name>' /var/log/<app>/<app>.log | tail -n 200 # read-only; surfaces "session not found" or "anonymous" classifications bracketing the loop window.
ss -ltnp 'sport = :<port>' | head -n 20 # read-only; obtain <pid> from the 'pid=' field of the listener line (example: ss -ltnp 'sport = :443' | awk '/LISTEN/{print $0}').
ps -o pid,cmd -p <pid> 2>/dev/null | head -n 5 # read-only; confirms which process owns the listener and which config file it loaded, without restarting anything.