4.8 KiB
7. SameSite, Secure, and the cookie that is never stored
Prev: 6. CSRF for SPAs · Next: 8. Debugging recipes
CORS decides whether the browser lets your JavaScript read a response. SameSite decides whether the browser sends the cookie in the first place. Getting CORS perfect and SameSite wrong produces a request that arrives cleanly and is anonymous.
What Spring emits by default
From docs/output/12-samesite.txt, under csrf.spa():
Set-Cookie: XSRF-TOKEN=<token>; Path=/
Set-Cookie: JSESSIONID=<session>; Path=/; HttpOnly; SameSite=Lax
The session cookie gets SameSite=Lax from Boot's
server.servlet.session.cookie.same-site default. The CSRF cookie gets no SameSite attribute
at all: CookieCsrfTokenRepository's default cookie customizer is, in bytecode, a single
return. Nothing is set.
An absent SameSite is not "no restriction". Chromium-based browsers treat it as Lax; Firefox has
not enabled Lax-by-default on its release channel (network.cookie.sameSite.laxByDefault is on in
Nightly only). The two disagree, which is why "it works in Firefox and not in Chrome" is so often a
missing SameSite attribute. SpecCookieJar models the Chromium behaviour, because that is the one
you have to survive.
The two rules that matter
Storage (RFC 6265bis §5.5). "If the cookie's same-site-flag is None and the
cookie's secure-only-flag is false, then abort these steps and ignore the newly created cookie
entirely."
SameSite=None without Secure is not a weaker cookie. It is not a cookie. No console warning
is required, no error is raised, and the server has no idea.
Sending (RFC 6265bis §5.8.3). Strict and Lax cookies are not attached to cross-site
requests, except that Lax allows top-level safe-method navigations. A fetch() from a SPA is a
subresource request, not a top-level navigation, so Lax does not help it.
Running the rules instead of quoting them
SpecCookieJar implements those two paragraphs in about sixty lines, and
/diag/cookie-spec feeds the application's own Set-Cookie headers through it. Over a
trustworthy origin:
Set-Cookie |
Stored? | Sent on a cross-site fetch? |
|---|---|---|
JSESSIONID=s1; HttpOnly; SameSite=Lax |
yes | no |
JSESSIONID=s2; HttpOnly; SameSite=None |
no | — |
JSESSIONID=s3; Secure; HttpOnly; SameSite=None |
yes | yes |
XSRF-TOKEN=t1 (no SameSite) |
yes | no |
XSRF-TOKEN=t2; SameSite=None |
no | — |
XSRF-TOKEN=t3; Secure; SameSite=None |
yes | yes |
Two of six reach a cross-site fetch, and they are the two carrying both attributes.
The trap that costs a day: plain http during development
Secure is only honoured from a trustworthy origin. Over plain http the attribute is
discarded, which makes SameSite=None; Secure collapse into SameSite=None with no Secure
— which is then rejected outright. The first block of 12-samesite.txt is that: nothing
survives.
http://localhost is treated as trustworthy by current browsers, so it works. http://127.0.0.1
and http://192.168.x.x are not, and do not. A developer testing a cross-site SPA against a LAN
address will find that the cookie simply never appears, with no message anywhere.
The configuration
Boot writes exactly what you tell it, and does not add Secure for you when you ask for
none:
server:
servlet:
session:
cookie:
same-site: none
secure: true # required. Omit it and the cookie is discarded by the browser.
http-only: true
Spring Security's CSRF cookie is separate and needs its own customizer:
CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
repository.setCookieCustomizer((cookie) -> cookie.sameSite("None").secure(true));
http.csrf(csrf -> csrf.spa().csrfTokenRepository(repository));
Order matters — chapter 6.
Partitioned cookies (CHIPS)
Partitioned requires Secure and, in practice, SameSite=None. It changes the cookie's
storage key so that a cookie set in a third-party context is scoped to the top-level site that
embedded it. SpecCookieJar models the Secure requirement; it does not model partitioning,
which is noted here rather than pretended.
If your SPA and API are separate registrable domains, the honest conclusion is:
A same-site deployment removes this entire chapter. Serving the SPA and the API from one origin, or from two subdomains of one registrable domain, means
SameSite=Laxworks,Secureis a hygiene setting rather than a prerequisite, and the preflight disappears. A reverse proxy in front of both is usually less work than everything above.
Prev: 6. CSRF for SPAs · Next: 8. Debugging recipes