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.
# 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, orSECRET_KEYissues. - JWT-based auth commonly breaks due to bad clock sync, wrong issuer/audience, proxy header issues, missing
Authorizationforwarding, or stale signing keys. - OAuth and third-party auth flows often fail because callback URLs, cookie scope, or
SameSitesettings 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.
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.
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:
SecureHttpOnlySameSiteDomainPathMax-AgeorExpires
Bad examples:
Set-Cookie: session=abc; SameSite=NoneModern browsers reject this unless Secure is also set.
Set-Cookie: session=abc; Domain=wrongdomain.comBrowser will not store it for your current host.
Safer default:
Set-Cookie: session=abc; Path=/; HttpOnly; Secure; SameSite=Lax3. 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=NonewithoutSecure- 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.
curl -v https://yourapp.com/protected 2>&1 | grep -Ei '< set-cookie|> cookie|location:'If the cookie is stored but not sent, check:
Pathdoes not match protected routeDomaindoes not match current hostSecurecookie is being used over HTTPSameSiteblocks the request context
5. Check proxy config
Make sure your reverse proxy forwards the original host and scheme.
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:
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
gunicorn app:app --forwarded-allow-ips='*'Flask with ProxyFix
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.
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:
printenv | grep -Ei 'secret|session|cookie|csrf|jwt|domain|secure|samesite'Search app config:
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:
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.
python -c "import jwt; t='PASTE_TOKEN'; print(jwt.decode(t, options={'verify_signature': False}, algorithms=['HS256','RS256']))"Verify:
issaudexpnbfkid- signing algorithm
- current server time
Time checks:
date -u
timedatectl statusJWT 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
SameSiteis 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_SECUREenabled while part of the flow still uses HTTPSameSitepolicy blocking cookies after OAuth, external redirect, or subdomain hop- cookie
Domainset incorrectly for production host SECRET_KEYdiffers between app instances or changed after deploy- server-side session storage is not shared across instances
- Nginx or load balancer not forwarding
HostorX-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
expornbfvalidation failure Authorizationheader 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
Pathattribute prevents cookie from being sent to protected routes
Debugging tips
Compare local success and production failure side by side.
Useful commands:
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-pagerAdditional 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.
| Attribute | Strict / Host-only | Lax / Parent-domain | None |
|---|---|---|---|
| Use case | Session cookies, CSRF protection | Navigation cookies | Cross-site embeds / OAuth |
| HTTPS required | No | No | Yes (Secure flag required) |
| Cross-site sends | Never | Top-level GET only | Always |
| CSRF risk | None | Low | High — mitigate with tokens |
cookie decision table for host-only vs parent-domain, Lax vs None, and HTTPS requirements.
Checklist
- ✓ Login response includes
Set-Cookieor returns a valid JWT. - ✓ Browser stores the cookie successfully.
- ✓ Subsequent requests send the cookie or
Authorizationheader. - ✓
SECRET_KEYor 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, andPathsettings 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
- Deploy SaaS with Nginx + Gunicorn
- HTTPS Setup (Let’s Encrypt)
- Protecting Routes and APIs
- Handling User Logout Correctly
- SaaS Production Checklist
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:
- cookie or JWT issuance
- browser storage
- cookie/header return on next request
- proxy headers and HTTPS awareness
- CSRF and origin checks
- shared secrets and session backend consistency
Keep auth config explicit, identical across instances, and covered by post-deploy smoke tests.