Authentication · intermediate

Session-cookie checklist

A Session-cookie checklist for engineers diagnosing authentication failures where browser sessions disappear or fail to reach the server. The guide frames a clear argument: most session loss is not a "session bug" but a contract mismatch between the cookie the browser sends and the request the server accepts, so triage must verify attributes, scope, transport, and storage before changing application code. Use it when users report being signed out unexpectedly, when authenticated requests return 401/302 to login, or when a SameSite or Secure change silently drops cookies.

The symptoms

  • User is silently signed out on a page reload even though the login response appeared to succeed.
  • Authenticated API calls return 401 or 302 to a login endpoint only on first request after navigation, not on subsequent fetches.
  • Session works on http:// but fails the moment the site is loaded over https://, or vice versa, depending on attribute configuration.
  • Embedded or third-party context (iframe, webhook receiver, OAuth callback) loses the session cookie that works in the top-level origin.
  • Developers reproduce the bug in an incognito window but not in their normal browser, hinting at profile, extension, or stored-cookie differences.
  • Mobile browser or older desktop browser shows the session as missing even though DevTools shows the cookie present.

Likely causes

  • The Set-Cookie response lacks Secure, so the browser refuses to attach it on TLS origins, producing intermittent 401s that look like session expiry.
  • The Set-Cookie response lacks HttpOnly, SameSite=None, or partitioned storage, and a cross-site navigation drops the cookie per current browser default behavior.
  • Path or Domain attributes scope the cookie to a narrower origin than the API endpoint, so the browser sends the cookie on the marketing host but not on the authenticated API host.
  • Two Set-Cookie headers with overlapping names are emitted without a deletion step, so the browser keeps the older, narrower value and ignores the refreshed one.
  • An intermediate proxy, CDN, or web application firewall strips or rewrites Set-Cookie, often because the response is gzip or brotli compressed and a downstream parser misreads the attribute boundary.
  • SameSite is set to None without Secure, which modern browsers ignore by default; the cookie never attaches on cross-site requests, mimicking a session timeout.
  • Server-side session store has expired the record even though the cookie is still within Max-Age, usually due to clock skew between the issuing server and the store writer.

First ten minutes

  1. 01Capture the exact failing request and response in the browser DevTools Network panel, including the Set-Cookie response header and the Cookie request header on the next authenticated request; this is the single most decisive piece of evidence.
  2. 02Read the Set-Cookie attributes literally: Domain, Path, Expires or Max-Age, Secure, HttpOnly, SameSite, and any Partitioned or Priority hint; write them down before drawing conclusions.
  3. 03Compare the request URL scheme (http vs https), host (exact registrable domain), and path prefix against the cookie's Domain and Path attributes to see whether the browser is contractually allowed to attach it.
  4. 04Reproduce in a fresh incognito window with no extensions; if the failure disappears, the cause is stored cookie state, profile corruption, or an extension rewriting headers.
  5. 05Check the server access log for the same timestamp for the response that set the cookie and the subsequent request that lost it, focusing on whether the Cookie header was present on the wire.
  6. 06If a reverse proxy, CDN, or WAF sits in front of the app, look for documented cookie handling on that component before suspecting application code.
  7. 07Decide whether the symptom is "cookie not sent" (attribute or scope mismatch) or "cookie sent but rejected" (expired store entry, signature mismatch, rotated key) before changing anything.

Evidence to collect

  • The raw Set-Cookie response header from the login response, including every attribute, copied verbatim with no normalization.
  • The raw Cookie request header on the first request after login that returns 401 or 302, and on the first request that succeeds, for direct comparison.
  • The request URL scheme, host, and path of the failing endpoint, used to compare against cookie Domain and Path.
  • Server log lines for the issuing response and the next authenticated request, with timestamps and request identifiers aligned.
  • Any intermediate proxy, CDN, or WAF configuration that rewrites, compresses, or filters Set-Cookie, identified by component name and version if available.
  • Browser and version where the failure is observed, because attribute defaults differ across recent releases.

Where to look

  • The HTTP boundary between the browser and the first proxy, where Set-Cookie is emitted and where Cookie must be attached on the next request.
  • The origin-to-CDN hop, where compression, header rewriting, or cookie-based routing policies may drop or replace Set-Cookie values.
  • The application session middleware boundary, where the inbound Cookie is parsed, the session identifier is resolved, and any session-store lookup happens.
  • The session store boundary, where the session record must still exist, be unexpired, and be addressable by the exact identifier the middleware extracted.
  • The embedded-context boundary, iframes, federated login callbacks, or third-party widgets, where SameSite and partition semantics differ from top-level navigation.

Diagnostic steps

  1. 01Verify Domain and Path on Set-Cookie against the host and path prefix of the API endpoint that returns 401; a narrower scope than the request is the most common cause of "browser sessions disappear."
  2. 02Verify Secure matches the page scheme; a cookie marked Secure will not be sent on plain http origins, and an insecure cookie is increasingly rejected on https origins.
  3. 03Verify SameSite value against the cross-site context of the failing request; SameSite=Lax blocks the cookie on cross-site POSTs and subresource loads, which can look like a session failure.
  4. 04Verify the server-side session store still contains a record for the identifier in the cookie by reading the store directly using the identifier captured from DevTools; absence here means the bug is storage, not cookie attributes.
  5. 05Verify response compression and any intermediary that parses Set-Cookie by inspecting the response Content-Encoding header and the proxy's header handling; compressed Set-Cookie parsing is a known source of attribute loss.
  6. 06Verify the session middleware signature or encryption key has not rotated mid-session by checking that the same key material is used at issue time and at verify time; a key change invalidates otherwise-valid cookies.
  7. 07Verify behavior in a private window without extensions, then re-enable extensions one at a time to isolate any privacy or cookie-blocking extension that strips Set-Cookie or Cookie.

Common mistakes

  • Concluding the session middleware is broken before confirming whether the browser actually attached the Cookie header; the middleware cannot read a cookie that never arrived.
  • Adding SameSite=None without Secure and assuming cross-site contexts will now work; modern browsers refuse this combination, so the cookie simply is not sent.
  • Changing the cookie's Domain from a subdomain to the registrable domain (or vice versa) without verifying that the issuing host and the consuming host align with the new scope.
  • Trusting the browser's "visible cookies" list as proof the cookie was sent; DevTools shows stored cookies, not necessarily the ones attached to a given request.
  • Assuming a successful login response proves the cookie is usable, without checking the next authenticated request's Cookie header on the wire.
  • Pinning the bug on "the browser" when an intermediary is rewriting Set-Cookie, which can be diagnosed only by comparing what the app sent with what reached the browser.

Safe fixes

  • If evidence shows the cookie is scoped too narrowly, issue a new Set-Cookie with Domain and Path covering the API host and prefix, and verify by inspecting the next request's Cookie header before declaring the fix.
  • If evidence shows the cookie is not sent cross-site because SameSite is Lax or omitted and the flow requires third-party context, set SameSite=None with Secure, only after confirming the page is served over TLS.
  • If evidence shows the cookie never reaches TLS because Secure is missing, add Secure and verify the login response and subsequent request both occur over https; do not rely on http redirects to upgrade.
  • If evidence shows the intermediary strips or rewrites attributes, change the proxy or CDN rule so Set-Cookie passes through unmodified, and re-capture the header to confirm attributes survive the hop.
  • If evidence shows the session record is gone from the store, do not extend cookie lifetime to compensate; address the store's expiration and clock-skew root cause, and verify with a fresh login followed by a store lookup using the captured identifier.
  • If evidence shows the signing key rotated mid-session, complete the key transition so cookies issued before and after the rotation are both verifiable, and verify by decoding a token from before the rotation.

Prove the fix

  1. 01In DevTools, the request immediately following a successful login includes the Cookie header with the session identifier and the expected attributes appear in Set-Cookie of the login response, both verified against the failing endpoint's host and path.
  2. 02The first authenticated request to the previously failing endpoint returns the expected 200 or 2xx response, with no 401 or 302 redirect to a login URL, in the same browser and incognito window where the bug reproduced.
  3. 03A direct lookup in the session store using the identifier from the captured Cookie header returns a non-expired record that matches the user's identity, confirming the cookie is both sent and accepted.
  4. 04A page reload of an authenticated page keeps the user signed in, and a hard refresh in a new tab also keeps the session, ruling out transient or in-memory-only state.
  5. 05If the failure was cross-site, the cookie attaches when the request is initiated from a different origin (iframe or redirect chain) under the documented SameSite value, verified by capturing the Cookie header on the cross-site request.

Prevention and next steps

  • Adopt a single cookie contract for the application: define required attributes, scope, and rotation behavior, and document them where the session middleware is configured so changes are reviewable.
  • Add an automated check that asserts the Set-Cookie attributes on the login response and asserts that the next authenticated request carries the Cookie header, run on every deployment.
  • Pin session-store expiration to a clock source shared with the issuing servers, and alarm on drift that would invalidate otherwise-valid cookies before their Max-Age.
  • Test the same login flow in an embedded or cross-site context that mirrors production, not only in the top-level origin, so SameSite and Partitioned regressions surface before release.
  • Document any intermediary that touches Set-Cookie and review its header-handling configuration whenever the proxy or CDN is upgraded.

Safe commands and checks

printf 'GET /protected HTTP/1.1\\r\\nHost: <your-api-host>\\r\\nCookie: <session-cookie-name>=<captured-value>\\r\\nConnection: close\\r\\n\\r\\n' | openssl s_client -connect <your-api-host>:443 -servername <your-api-host> -quiet
printf 'GET / HTTP/1.1\\r\\nHost: <your-origin-host>\\r\\nConnection: close\\r\\n\\r\\n' | openssl s_client -connect <your-origin-host>:443 -servername <your-origin-host> -quiet | grep -i 'set-cookie'
tail -n 200 -f /var/log/<app>/access.log | grep -E '<session-cookie-name>|/login|/protected'
grep -E 'Set-Cookie|Set-Cookie2' /var/log/<proxy-or-cdn>/response.log | tail -n 50
redis-cli -h <store-host> -p <port> GET 'sess:<captured-session-id>'