# 7. SameSite, `Secure`, and the cookie that is never stored *Prev: [6. CSRF for SPAs](06-csrf-for-spas.md) · Next: [8. Debugging recipes](08-debugging-recipes.md)* 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`](output/12-samesite.txt), under `csrf.spa()`: ``` Set-Cookie: XSRF-TOKEN=; Path=/ Set-Cookie: JSESSIONID=; 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`: ```yaml 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: ```java 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](06-csrf-for-spas.md) · Next: [8. Debugging recipes](08-debugging-recipes.md)*