Debugging Authentication

A symptom-to-cause table for the failures you will actually hit, and the tools that tell you which one it is.

Authentication bugs are frustrating because the browser often hides the cause. This is the lookup table.

Symptoms and causes

SymptomMost likely cause
Works in Postman, fails in the browserCORS or SameSite — Postman enforces neither
Login returns 200, next call is 401missing credentials: 'include', or the cookie was rejected
"No 'Access-Control-Allow-Origin' header"origin not allowlisted, or an error page skipped the CORS middleware
"Wildcard '*' not allowed with credentials"echo the exact origin instead of *
Preflight returns 401auth middleware mounted before CORS
"Header field authorization is not allowed"missing from Access-Control-Allow-Headers
Works in Chrome, fails in Safarithird-party cookie blocking
Cookie set but never sent backSameSite/Secure/Domain/Path mismatch
Logout leaves the user logged inclearCookie attributes differ from when it was set
JWT "invalid signature"wrong key, wrong algorithm, or a rotated kid
JWT valid but rejectedaud or iss mismatch, or clock skew
Random logouts under loadparallel refresh calls tripping reuse detection
Rate limit blocks everyonetrusted proxies unset — every request shares the proxy's IP

The three-step method

  1. Look at the request headers, not the console. Is Cookie or Authorization actually present? If not, the browser dropped it and the server is irrelevant.
  2. Reproduce with curl. If curl works and the browser does not, it is a browser policy — CORS, SameSite, or third-party cookies. That single comparison halves the search space.
  3. Check the preflight separately. A failing OPTIONS means the real request never happened.

Devtools, specifically

In the Network tab, open the failing request and read the Request Headers. Chrome also flags rejected cookies in the Application panel with the reason. A yellow warning triangle next to a cookie is the answer, not a decoration.

Example

Example · bash
# The single most useful comparison in cross-origin debugging

# 1. Does the server work at all?
curl -i https://dfg.com/api/notes -H "Authorization: Bearer $TOKEN"

# 2. Does it answer a browser-shaped preflight?
curl -i -X OPTIONS https://dfg.com/api/notes \
  -H "Origin: https://abc.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: authorization"

# 3. Does it return the credential headers on the ACTUAL request?
curl -i https://dfg.com/api/notes \
  -H "Origin: https://abc.com" -H "Authorization: Bearer $TOKEN" \
  | grep -i 'access-control'

# curl works + browser fails  →  it is a browser policy, not your handler.

When to use it

  • An engineer stops debugging server code after one curl call proves the API is fine and the browser is dropping the cookie.
  • A preflight returning 401 is traced to middleware ordering in under a minute using the OPTIONS curl command.
  • Intermittent production logouts are traced to six parallel refresh calls firing on dashboard load.

More examples

A diagnostic endpoint for cross-origin problems

Distinguishing 'the browser never sent it' from 'the server refused it' is the fork the whole diagnosis hangs on, and this reports it directly.

Example · javascript
// Mount temporarily when a cross-origin problem is hard to pin down.
// It reports what the SERVER received — which is the fact everyone is guessing at.
app.get('/debug/auth', (req, res) => {
  res.json({
    origin: req.get('origin') ?? null,
    referer: req.get('referer') ?? null,

    // Was a credential actually attached?
    hasCookieHeader: Boolean(req.get('cookie')),
    cookieNames: Object.keys(req.cookies ?? {}),
    hasAuthorization: Boolean(req.get('authorization')),
    authScheme: (req.get('authorization') ?? '').split(' ')[0] || null,

    // What the rate limiter and Secure-cookie logic will see
    ip: req.ip,
    forwardedFor: req.get('x-forwarded-for') ?? null,
    protocol: req.protocol,
    secure: req.secure,

    // What this server thinks it allows
    allowedOrigins: [...ALLOWED_ORIGINS],
    originAllowed: ALLOWED_ORIGINS.has(req.get('origin') ?? ''),

    serverTime: new Date().toISOString(),   // for JWT clock-skew questions
  });
});

// From the browser console on https://abc.com:
//   await (await fetch('https://dfg.com/debug/auth', {credentials:'include'})).json()
//
// hasCookieHeader: false  → the BROWSER dropped it (SameSite / 3rd-party / credentials)
// hasCookieHeader: true, still 401 → the server rejected it (expired / unknown session)
// secure: false behind a proxy → trust proxy is unset, so Secure cookies will not stick
//
// Remove this route before it ships. It is a debugging aid, not a feature.

Reading cookie rejections in Chrome

Chrome states the exact reason a cookie was rejected. Reading that message directly is faster than any amount of reasoning about your configuration.

Example · bash
# DevTools → Application → Cookies
#   A yellow triangle next to a cookie IS the answer. Hover it.

# DevTools → Network → the request → Cookies tab
#   Shows cookies that were BLOCKED and why:
#
#   "This Set-Cookie was blocked because it had the SameSite=None attribute
#    but did not have the Secure attribute"
#     → add Secure
#
#   "This cookie was blocked because it is a third-party cookie and
#    third-party cookie blocking is enabled"
#     → cross-site cookies are not viable for this user; use a token
#
#   "This Set-Cookie was blocked because its Domain attribute was invalid
#    with regards to the current host url"
#     → dfg.com cannot set a cookie for abc.com. It never can.
#
#   "This attempt to set a cookie via Set-Cookie was blocked because it had
#    the Secure attribute but was not received over a secure connection"
#     → you are on http, or trust proxy is unset behind a TLS terminator

# Test in the browsers that matter, deliberately:
#   Chrome, third-party cookies blocked  (Settings → Privacy)
#   Safari                               (blocks by default)
#   Firefox                              (partitions by default)
#   Any browser in private mode          (stricter than normal)

Discussion

  • Be the first to comment on this lesson.