1
0
Files
spring-security-demo/cors-csrf/docs/07-samesite.md
2026-08-28 10:16:21 +05:30

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=Lax works, Secure is 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