112 lines
4.8 KiB
Markdown
112 lines
4.8 KiB
Markdown
# 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=<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`:
|
|
|
|
```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)*
|