CSRF & Auth Errors
These were the hardest class of bug to track down when first standing up the API behind a real domain — each fix exposed the next, deeper layer of the same underlying issue.
Bare 403 on every mutating request, no JSON body
Symptom: POST/PUT/PATCH/DELETE all return a bare 403 with no
response body, in both local dev and production. GET requests and login
work fine.
Cause: Spring Security’s default CSRF token handler only writes the CSRF cookie into the response when something explicitly reads the CSRF token attribute mid-request. Server-rendered form views do this automatically; a pure JSON API with no view layer never triggers it — so the cookie is never set, the frontend has nothing to attach as a header, and every mutating request is rejected before it reaches a controller.
Fix: add a filter that forces eager token resolution on every request — reads the CSRF token attribute and calls its getter, unconditionally. This is the standard documented workaround for CSRF on a pure-API (non-template) Spring Security setup.
Still 403 even though the CSRF cookie is visibly present in the browser
Symptom: DevTools shows the CSRF cookie exists, but the failing
request’s actual Cookie header (in the Network tab) doesn’t include it.
Cause: the frontend and API are on different subdomains. A cookie set
with no explicit Domain attribute is scoped only to the exact hostname
that set it — JavaScript running on a different subdomain can never read it
via document.cookie, regardless of SameSite settings. This is a browser
cookie-scoping rule, not a CSRF or CORS setting.
Fix: set an explicit Domain attribute on the CSRF cookie so it’s
shared across subdomains of the same registrable domain, via a configurable
property — left empty for local dev (where both sides run on localhost
and no domain scoping is needed).
500 error on login: invalid cookie domain
Symptom: after fixing the above, login itself starts throwing a 500 with an “invalid domain” exception.
Cause: the cookie-domain value had a leading dot (the old convention for “match this domain and all subdomains”). Modern strict cookie processors enforce RFC 6265, which does not allow a leading dot at all — it throws rather than silently stripping it. Under RFC 6265, a domain with no leading dot already implies “this domain and all its subdomains” — the dot isn’t just unnecessary, it’s invalid.
Fix: remove the leading dot from the cookie-domain value.
This exception only surfaces at runtime, on the first request that actually tries to save a CSRF token cookie (e.g. during login) — it does not fail at application startup. Always test an actual login end-to-end after changing this kind of config, not just a health check.