Authentication · advanced

Login redirects forever after deploy: compare trusted origins and cookie scope

After a redeploy that changes the public origin (scheme, hostname, or port) of a web application, users hit an infinite login redirect loop on the new host while authentication still works on the old host. The loop is almost always a mismatch between the origins the auth server accepts as return targets and the cookie scope it issued earlier. This guide frames the failure as a boundary problem between identity provider (IdP), reverse proxy, and application origin, and walks through evidence-based comparison of trusted origin allowlists against cookie attributes (Domain, Path, Secure, SameSite) to break the loop without disabling security controls.

The symptoms

  • Users authenticated on the previous origin are bounced back to the login page from the new origin; the URL shows repeated alternation between /login and /callback (or /sso, /oidc/callback, /auth/redirect) with the same query parameters each cycle.
  • The IdP logs the user out or starts a new auth code request on every cycle, while a valid `id_token` or session JWT is visible in the browser dev tools for the previous origin but not for the new one.
  • Browser dev tools Application > Cookies panel shows the session cookie present on the old host but absent or filtered on the new host, even when both hosts serve the same application code.
  • Network panel shows a 302 chain: user agent → IdP authorize → IdP redirect back to app callback → 302 to login → repeat, with no explicit 401, 403, or CSRF error body between steps.
  • Symptoms disappear for users on the old origin, for users with a hard refresh in a private window on the new origin, and for any environment whose auth config still matches the new origin's hostname.

Likely causes

  • Trusted origin/redirect URI mismatch: the IdP or auth middleware has an allowlist (for example `Allowed Callback URLs`, `Valid Post Logout Redirect URIs`, `OAuth Redirect URIs`, SAML `ACS` URL) that does not include the new public hostname or scheme, so the callback is rejected and the app re-initiates auth.
  • Cookie scope narrower than the new origin: a `Set-Cookie` header from the auth layer restricts `Domain` to the old host (e.g. `.old.example.com`) or `Path` to a route not present on the new origin, so the browser refuses to attach the session cookie on the new origin's requests.
  • Cookie attribute contradictions: `Secure` flag is set but the new origin is served over HTTP behind a proxy that terminates TLS, or `SameSite=None` is required for cross-site IdP redirects but the cookie is issued `SameSite=Lax`/`Strict`, causing the browser to withhold it on the cross-origin round trip.
  • Reverse proxy or CDN rewriting the public origin: an ingress terminates TLS and forwards to the app with an internal `Host` header or `X-Forwarded-Proto: http` that differs from the user's view of the origin, so the app's origin-compare logic (used for CSRF/state checks or redirect validation) sees a different origin than the browser.
  • Front-channel logout or session store pinned to the old origin: a session that was issued with a `Domain` or `Partition Key` keyed on the old hostname is still valid in the IdP's view but unreadable for the new origin, so each call to the app's session lookup returns null and a new login is forced.

First ten minutes

  1. 01Reproduce on one browser profile: open the new public URL in a private/incognito window, sign in once, and watch the address bar for a `redirect_uri` or `RelayState` value that ping-pongs between the IdP and the app. Capture the exact callback URL the IdP is sending the user back to.
  2. 02Open dev tools Application > Cookies for the new origin and confirm the session cookie is either missing or present but not sent on the callback request (visible in the Network panel's Cookie header). Compare with the old origin's Cookies panel in another tab.
  3. 03Pull the `Set-Cookie` header from the IdP's response on the new origin and read the raw attributes — `Domain`, `Path`, `Secure`, `HttpOnly`, `SameSite`, `Expires`/`Max-Age` — to see whether the cookie's scope can legally cover the new hostname under RFC 6265.
  4. 04Diff the IdP tenant config between the new origin and a known-good origin (for example, a staging or pre-deploy environment), focusing on any `Allowed Callback URLs`, `Allowed Web Origins`, `Allowed Logout URLs`, `Trusted Origins`, or CORS `Origin` allowlist fields; the new hostname must appear verbatim, including scheme and trailing path.
  5. 05Check the reverse proxy and ingress configuration for `X-Forwarded-Host`, `X-Forwarded-Proto`, and the trusted `Host` header, because a mismatch between the public hostname and the value the app reads as `origin` will trip origin-compare checks even when the IdP config looks correct.

Evidence to collect

  • The complete 302 chain from the new origin's Network panel, including every `Location:` header and `Set-Cookie` header in order, plus the matching query string on the callback request (`code`, `state`, `RelayState`, `SAMLResponse`).
  • Hex or base64-decoded contents of the auth `state` / `nonce` / `RelayState` parameter, which usually encodes the intended callback URL the app expects versus the URL it actually received.
  • Raw `Set-Cookie` headers issued by both the IdP and the application on the new origin, including attributes for scope, with timestamps so expiry windows can be compared.
  • IdP-side audit event for each failed callback, ideally with `redirect_uri` and `client_id` fields, to distinguish "origin not allowed" from "session not found" from "state mismatch (possible CSRF rejection)".
  • The reverse proxy's view of `Host`, `X-Forwarded-Host`, and `X-Forwarded-Proto` on the callback request, side by side with the public URL the browser used, to detect origin-rewriting at the edge.
  • Previous and current deployment manifests (ingress hostname, CDN custom domain, app config keys such as `PUBLIC_URL`, `APP_ORIGIN`, `TRUSTED_HOSTS`, `COOKIE_DOMAIN`) so a textual diff shows exactly which origin-related values changed.

Where to look

  • Identity provider admin console: Application / API / Client settings pages that list allowed callback URLs, allowed origins (CORS), allowed logout URLs, and — for SAML — `ACS` and `SingleLogoutService` URLs. Look for the exact new hostname string, including scheme, port, and any path prefix.
  • Application configuration sources: env vars, secrets manager entries, Helm/Compose values, and feature flags that drive redirect validation, cookie issuance, and trusted-host middleware. Boundary to inspect is everything that names an `Origin` or `Domain`.
  • Reverse proxy / ingress layer: `Host` header parsing, `X-Forwarded-Host` and `X-Forwarded-Proto` propagation, request-redirect and URL-rewrite rules, and any `proxy_set_header` lines that pin a public hostname.
  • Browser boundary: dev tools Application > Cookies (Storage panel) for the new origin, Network panel `Set-Cookie` responses, and Console for `SameSite`/`Secure` warnings surfaced as "cookie blocked because…" messages.
  • Session store / cache: keys or partitions keyed on hostname or `Domain`, to verify whether the session issued on the previous origin is resolvable under the new origin's key.

Diagnostic steps

  1. 01Capture the exact callback URL the IdP is using to redirect back to the app on the new origin, then byte-compare it to every entry in the IdP's allowed-redirect-URI list. A difference in scheme (`http` vs `https`), port, trailing slash, or path prefix is sufficient to cause a hard reject that manifests as a new auth request and a redirect loop.
  2. 02Read the RFC 6265 cookie-domain matching rules and apply them to the session cookie's `Domain` attribute against the new origin's registrable domain. A cookie set for `Domain=.old.example.com` will not be sent to `new.example.com` even when both are owned by the same organisation.
  3. 03For cross-origin IdP flows, confirm the session cookie's `SameSite` allows the auth round trip. `SameSite=None` requires `Secure=true`; `SameSite=Lax` allows top-level GET navigations but withholds the cookie on the IdP's cross-site POST back to the app's callback if that POST is not top-level.
  4. 04Inspect the app's trusted-origin or origin-compare middleware (frameworks commonly name it `TRUSTED_HOSTS`, `ALLOWED_HOSTS`, `CORS_ORIGIN`, or `app.use(csrf())`-style state validation) and confirm the public origin the browser sees equals the origin the app validates against after the reverse proxy, by logging both values on one request.
  5. 05Decide which side of the boundary owns the configuration change: IdP admin (redirect URIs, allowed origins), app config (cookie domain, trusted hosts, public URL), or proxy/CDN (forwarded headers, URL rewrites). Hold all three in mind together because a fix on one side without the others may not break the loop.

Common mistakes

  • Adding only the bare hostname (e.g. `new.example.com`) to the IdP allowlist when the app actually posts back to `https://new.example.com/auth/callback`; the auth server's exact-string match treats the difference as a different URI and rejects it silently.
  • Setting the session cookie `Domain=.old.example.com` once during initial setup and then changing the public hostname later; browsers do not rewrite cookie scope on origin change, so the cookie remains bound to the old registrable domain and is silently dropped on the new origin.
  • Writing `SameSite=None` but forgetting `Secure`, or terminating TLS at the proxy so the app sees a plain HTTP origin; the cookie is either refused outright by the browser or issued but never attached to subsequent requests.
  • Trusting the IdP's "sign-in successful" page without inspecting the `redirect_uri` query parameter; a validation failure at the IdP can produce an IdP-side success screen while the app's callback still receives an error and triggers a fresh login.
  • Restarting the app or clearing the IdP tenant configuration cache before the new redirect URI is verified end-to-end, masking whether the change took effect and creating intermittent loop vs. no-loop behaviour across deploys.

Safe fixes

  • Add the full callback URL (scheme + host + port + path) of the new origin to the IdP's allowed redirect URI list, then save and wait for any tenant-config cache TTL shown in the IdP documentation to elapse before re-testing.
  • Issue a new session cookie scoped to a registrable domain that covers the new origin (for example `Domain=.example.com` instead of `Domain=.old.example.com`), and add a one-time, scoped session invalidation for the old origin so users are not left holding two unsynchronised cookies; coordinate the cookie change with an app redeploy.
  • If the auth flow is cross-site, issue the session cookie as `SameSite=None; Secure` and confirm the new origin is reachable exclusively over HTTPS, including behind the reverse proxy; never relax `Secure` to satisfy `SameSite`.
  • Reconcile the reverse proxy so the app reads the public origin it actually serves: pass through `Host`, set `X-Forwarded-Proto` to the public scheme, and list the public hostname in `TRUSTED_HOSTS`/`ALLOWED_HOSTS` so origin-compare middleware does not silently rewrite or reject the request.
  • For SAML deployments, update both the IdP's `ACS` URL and the app's `SP` entity metadata to the new origin, and verify that signed assertions are still accepted after the entity-ID change so logout and renewal flows do not reintroduce the loop.

Prove the fix

  1. 01In a private browsing window with no pre-existing cookies, complete one full login on the new origin, close the tab, reopen the protected page, and confirm the user remains signed in without any intermediate 302 to the IdP; this proves the cookie scope covers the new origin.
  2. 02Inspect the IdP's audit log for the test session and confirm the recorded `redirect_uri` exactly matches the new origin's full callback URL, including scheme, port, and path; any mismatch means the loop can recur on the next deploy.
  3. 03Repeat the same private-window flow in a second browser that has an old-origin cookie still set, and confirm the user is silently upgraded to a new-origin session rather than chained into a new auth request; this proves the old-origin cookie is no longer a gate.
  4. 04Block direct access to the callback path with a forged `Host` header (or change the proxy to drop it) and confirm the app's trusted-host check returns a 4xx rather than issuing a redirect; a 302 here would re-create the loop on the next misconfigured edge.
  5. 05Diff the live `Set-Cookie` header on the new origin against the previous origin and confirm `Domain`, `Path`, `Secure`, `HttpOnly`, and `SameSite` are all consistent with the documented intent; the diff is the regression artefact for the next deploy.

Prevention and next steps

  • Treat origin, redirect URI, and cookie scope as first-class configuration that lives in version control and is reviewed in every change that touches public hostnames, schemes, or domains; deploys should not be able to advance when these fields drift out of sync.
  • Run a pre-deploy smoke check that initiates an auth round trip against the candidate public hostname in a synthetic browser profile and fails the release if the callback URL is not in the IdP allowlist or the issued cookie is not sent on a follow-up protected request.
  • Choose cookie `Domain` at the level of the registrable domain you intend to keep, not the level of the current hostname, and document the choice so future host renames do not require a cookie re-issue to break a loop.
  • Keep the reverse proxy and the application's trusted-host list derived from a single source of truth, so `Host`/`X-Forwarded-Host`/`X-Forwarded-Proto` cannot drift from what the application code treats as the public origin.
  • Capture the redirect chain and `Set-Cookie` attributes in deployment evidence so post-deploy login regressions can be diffed against the previous release in seconds rather than reconstructed from browser memory.

Safe commands and checks

Inspect a captured `Set-Cookie` header against the RFC 6265 attribute list by piping the header value through `awk -F'; ' '{for (i=1;i<=NF;i++) print i, $i}'`, then verify each attribute (`Domain`, `Path`, `Secure`, `HttpOnly`, `SameSite`, `Expires`/`Max-Age`) against your documented intent.
Diff two configuration files that each list auth-related origins with `diff -u <old-config> <new-config>` and restrict the comparison to lines matching `origin|redirect|callback|cookie|trusted|host` with `grep -E`, so only the relevant rows of an IdP admin export are reviewed.
Decode a base64url-encoded `state` or `RelayState` parameter captured from the failing callback by piping the value through `printf '%s' "<captured-state>" | base64 -d` (substituting `-d` for `-D` as required by the host platform), then read the embedded intended callback URL to compare against what the IdP actually returned.
Quote a JSON config key from a configuration audit export with `jq -r '.trusted_origins[]' <config.json>` and pipe it through `sort -u` so the IdP-side allowlist and the app-side trusted-host list can be diffed character-for-character before the next deploy.
Print the cookie-attribute string with attribute separators made visible by piping through `tr '; ' '\n'` after stripping the `name=value` prefix; use the output to confirm `Domain` covers the new origin's registrable domain and that `SameSite` is consistent with the cross-site nature of the auth round trip.