Session/Auth Issues in Production

The essential playbook for implementing session/auth issues in production in your SaaS.

Use this page when authentication works locally but fails in production. Typical symptoms include users being logged out on refresh, session cookies not being set, CSRF errors, OAuth callback failures, JWTs rejected behind a proxy, or auth breaking after switching domains or enabling HTTPS.

Quick Fix / Quick Setup

Start by confirming whether the browser receives and sends the session cookie. Most production auth issues come from wrong cookie flags, mismatched domains, missing proxy headers, secret key rotation, or HTTPS/CSRF misconfiguration.

bash
# 1) Inspect auth-related response headers
curl -I https://yourapp.com/login
curl -k -I https://yourapp.com/login

# 2) Test whether Set-Cookie is present after login POST
curl -i -X POST https://yourapp.com/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"test@example.com","password":"testpass"}'

# 3) Verify proxy headers reach the app
curl -I https://yourapp.com | grep -Ei 'set-cookie|location'

# 4) Check server env for cookie/session config
printenv | grep -Ei 'secret|session|cookie|csrf|jwt|domain|secure|samesite'

# 5) Common production-safe cookie settings
# Flask
SESSION_COOKIE_SECURE=True
SESSION_COOKIE_HTTPONLY=True
SESSION_COOKIE_SAMESITE='Lax'

# FastAPI / Starlette response.set_cookie(...)
secure=True
httponly=True
samesite='lax'

# If behind Nginx / proxy, trust forwarded proto
# Gunicorn
gunicorn app:app --forwarded-allow-ips='*'

# Nginx
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

What’s happening

Production auth failures usually happen at the boundary between browser, app, and reverse proxy.

  • The app may authenticate correctly but fail to persist state because the cookie is blocked, not stored, not sent back, or cannot be verified.
  • Session-based auth commonly breaks due to Secure, SameSite, Domain, Path, CSRF, or SECRET_KEY issues.
  • JWT-based auth commonly breaks due to bad clock sync, wrong issuer/audience, proxy header issues, missing Authorization forwarding, or stale signing keys.
  • OAuth and third-party auth flows often fail because callback URLs, cookie scope, or SameSite settings are wrong after moving to HTTPS or a new domain.
  • Reverse proxies often hide the original scheme and host unless headers are forwarded and trusted by the app.
browser
Nginx
app
session store

request flow diagram showing browser -> Nginx -> app -> session store, with cookie attributes and forwarded headers highlighted.

Step-by-step implementation

1. Verify cookie issuance

After successful login, inspect response headers for Set-Cookie.

bash
curl -i -X POST https://yourapp.com/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"test@example.com","password":"testpass"}'

If Set-Cookie is missing:

  • login failed before session creation
  • app did not write the session
  • CSRF or validation blocked the request
  • API returns JWT but frontend expects cookie auth

2. Verify cookie attributes

Inspect:

  • Secure
  • HttpOnly
  • SameSite
  • Domain
  • Path
  • Max-Age or Expires

Bad examples:

http
Set-Cookie: session=abc; SameSite=None

Modern browsers reject this unless Secure is also set.

http
Set-Cookie: session=abc; Domain=wrongdomain.com

Browser will not store it for your current host.

Safer default:

http
Set-Cookie: session=abc; Path=/; HttpOnly; Secure; SameSite=Lax

3. Verify browser acceptance

In browser DevTools:

  • Network tab: inspect login response
  • Application/Storage tab: check whether cookie is stored
  • Console: look for blocked cookie warnings

Common browser-side rejection cases:

  • SameSite=None without Secure
  • invalid Domain
  • insecure origin with Secure
  • third-party context restrictions

4. Verify cookie return on next request

After login, inspect the next authenticated request and confirm the browser sends the cookie.

bash
curl -v https://yourapp.com/protected 2>&1 | grep -Ei '< set-cookie|> cookie|location:'

If the cookie is stored but not sent, check:

  • Path does not match protected route
  • Domain does not match current host
  • Secure cookie is being used over HTTP
  • SameSite blocks the request context

5. Check proxy config

Make sure your reverse proxy forwards the original host and scheme.

Nginx

nginx
location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Inspect effective config:

bash
sudo nginx -T | grep -Ei 'proxy_set_header|server_name|listen|ssl'

If these headers are missing, apps may:

  • generate wrong redirect URLs
  • fail CSRF origin checks
  • think requests are HTTP instead of HTTPS
  • mishandle secure cookies

6. Make the app trust proxy headers

Forwarding headers is not enough if the framework ignores them.

Gunicorn

bash
gunicorn app:app --forwarded-allow-ips='*'

Flask with ProxyFix

python
from werkzeug.middleware.proxy_fix import ProxyFix

app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

FastAPI / Starlette behind proxy

Use proxy-aware middleware or run Uvicorn/Gunicorn with forwarded headers enabled.

bash
uvicorn app:app --proxy-headers --forwarded-allow-ips='*'

7. Check session backend consistency

Random logout across requests usually means inconsistent secrets or non-shared session state.

Check env:

bash
printenv | grep -Ei 'secret|session|cookie|csrf|jwt|domain|secure|samesite'

Search app config:

bash
grep -RiE 'SECRET_KEY|SESSION_COOKIE|CSRF|JWT|cookie|samesite' .

If using multiple app instances:

  • all must use the same SECRET_KEY
  • all must use the same JWT signing key
  • all must reach the same Redis/database session store

Redis checks:

bash
redis-cli ping
redis-cli keys '*session*'

8. Check CSRF configuration

If login or form POST returns 403 in production, inspect CSRF and trusted origin config.

Common causes:

  • production domain missing from trusted origins
  • app sees wrong host or scheme because proxy headers are missing
  • frontend does not send CSRF token or cookies with credentials

For cookie-based cross-origin API requests, also check CORS:

  • Access-Control-Allow-Credentials: true
  • explicit allowed origin, not *
  • frontend uses credentials: 'include'

9. Check domain and subdomain behavior

If app runs on app.example.com and auth/API on api.example.com:

  • use host-only cookies unless you need shared auth across subdomains
  • if sharing is required, set a valid parent domain deliberately
  • verify frontend calls match the intended cookie scope

Default recommendation: do not set Domain unless necessary.

10. Check JWT validation

Decode a failing token and compare claims to config.

bash
python -c "import jwt; t='PASTE_TOKEN'; print(jwt.decode(t, options={'verify_signature': False}, algorithms=['HS256','RS256']))"

Verify:

  • iss
  • aud
  • exp
  • nbf
  • kid
  • signing algorithm
  • current server time

Time checks:

bash
date -u
timedatectl status

JWT production failures usually come from:

  • wrong issuer or audience
  • stale signing keys
  • clock drift
  • proxy stripping Authorization
  • different secrets between services

11. Check OAuth callback behavior

After enabling HTTPS or changing domains, callback URLs often stop matching exactly.

Verify:

  • provider callback URL matches production URL exactly
  • app generates https://... redirect URIs
  • cookies survive external redirect flow
  • SameSite is compatible with your auth flow

If OAuth fails only behind Nginx, the app likely does not trust forwarded HTTPS headers.

12. Retest with a clean session

After config changes:

  • use an incognito window
  • clear cookies
  • retry full login flow
  • test refresh, protected route access, logout, and callback flows

Common causes

  • SESSION_COOKIE_SECURE enabled while part of the flow still uses HTTP
  • SameSite policy blocking cookies after OAuth, external redirect, or subdomain hop
  • cookie Domain set incorrectly for production host
  • SECRET_KEY differs between app instances or changed after deploy
  • server-side session storage is not shared across instances
  • Nginx or load balancer not forwarding Host or X-Forwarded-Proto
  • framework not configured to trust proxy headers
  • CSRF trusted origins do not include production domain
  • JWT signing key, issuer, or audience mismatch between services
  • server clock drift causing JWT exp or nbf validation failure
  • Authorization header stripped by proxy or not sent by frontend
  • CORS credentials misconfiguration for cookie-based API auth
  • OAuth callback URL mismatch after domain or HTTPS change
  • mixed HTTP/HTTPS redirects causing secure cookie loss
  • Path attribute prevents cookie from being sent to protected routes

Debugging tips

Compare local success and production failure side by side.

Useful commands:

bash
curl -I https://yourapp.com/login
curl -i -X POST https://yourapp.com/login -H 'Content-Type: application/json' -d '{"email":"test@example.com","password":"testpass"}'
curl -I https://yourapp.com/protected
curl -v https://yourapp.com 2>&1 | grep -Ei '< set-cookie|> cookie|location:'
printenv | grep -Ei 'secret|session|cookie|csrf|jwt|domain|secure|samesite'
sudo nginx -T | grep -Ei 'proxy_set_header|server_name|listen|ssl'
grep -RiE 'SECRET_KEY|SESSION_COOKIE|CSRF|JWT|cookie|samesite' .
date -u
timedatectl status
python -c "import jwt; t='PASTE_TOKEN'; print(jwt.decode(t, options={'verify_signature': False}, algorithms=['HS256','RS256']))"
redis-cli ping
redis-cli keys '*session*'
journalctl -u nginx -n 200 --no-pager
journalctl -u your-app-service -n 200 --no-pager

Additional tips:

  • Log auth events with request host, scheme, forwarded proto, cookie names set, and redirect target.
  • Never log raw passwords or full JWTs.
  • Check browser console for blocked cookie and mixed content warnings.
  • If auth works on direct app port but fails through Nginx, focus on forwarded headers, HTTPS handling, or stripped headers.
  • If API auth fails only on browser requests, inspect CORS and credential handling.
  • If users are logged out after deploy, verify secrets and session storage first.
AttributeStrict / Host-onlyLax / Parent-domainNone
Use caseSession cookies, CSRF protectionNavigation cookiesCross-site embeds / OAuth
HTTPS requiredNoNoYes (Secure flag required)
Cross-site sendsNeverTop-level GET onlyAlways
CSRF riskNoneLowHigh — mitigate with tokens

cookie decision table for host-only vs parent-domain, Lax vs None, and HTTPS requirements.

Checklist

  • Login response includes Set-Cookie or returns a valid JWT.
  • Browser stores the cookie successfully.
  • Subsequent requests send the cookie or Authorization header.
  • SECRET_KEY or JWT signing key is identical across all app instances.
  • Session backend is shared and reachable if using server-side sessions.
  • App trusts proxy headers and sees the original HTTPS scheme.
  • Cookie Secure, SameSite, Domain, and Path settings match deployment topology.
  • CSRF trusted origins and host settings include production domains.
  • OAuth callback URLs exactly match provider configuration.
  • Server clock is synchronized for JWT validation.
  • No mixed HTTP/HTTPS redirects break cookie handling.
  • Auth logs and error tracking are enabled for failed login/session events.

Related guides

Additional useful references:

FAQ

Why is my session cookie not being set in production?

Check whether the login response includes Set-Cookie, whether cookie attributes are valid for the current domain and HTTPS state, and whether the browser rejects it due to SameSite=None without Secure or an invalid Domain.

Why do users get logged out randomly across requests?

This usually means app instances do not share the same SECRET_KEY or session backend, or a load-balanced setup depends on local in-memory session state.

Why does JWT auth fail only in production?

Common causes are wrong issuer/audience, unsynchronized server time, stale signing keys, missing Authorization forwarding through the proxy, or environment-specific secret mismatches.

Why do OAuth logins fail after enabling HTTPS?

The callback URL configured at the provider may no longer match exactly, or the app may not trust forwarded HTTPS headers and generates the wrong redirect_uri.

How do I choose between host-only cookies and domain cookies?

Use host-only cookies by default. Set a parent-domain cookie only when you intentionally need auth shared across subdomains and understand the security and debugging tradeoffs.

Why does login work locally but not in production?

Production adds HTTPS, reverse proxies, real domains, stricter cookie handling, and CSRF/origin checks that local development usually does not expose.

Why are users logged out after every deploy?

Your SECRET_KEY, session backend, JWT signing key, or container state may change between releases.

Should I use SameSite=None?

Only if you need cross-site cookie behavior, such as certain embedded or external redirect flows. It also requires Secure over HTTPS.

Do I need a cookie Domain setting?

Not always. Avoid setting Domain unless you explicitly need cookie sharing across subdomains.

Why does OAuth fail only after moving behind Nginx?

The app may not know the original HTTPS scheme or public host, causing wrong redirect_uri generation or failed secure cookie handling.

Final takeaway

Most production auth bugs are config mismatches, not authentication logic bugs.

Verify in this order:

  1. cookie or JWT issuance
  2. browser storage
  3. cookie/header return on next request
  4. proxy headers and HTTPS awareness
  5. CSRF and origin checks
  6. shared secrets and session backend consistency

Keep auth config explicit, identical across instances, and covered by post-deploy smoke tests.