Price the Storage Access API, and stop calling the header path cookie-free

Two more from the review. The section declared that no frontend change could
bring OAuth inside the criterion, which writes off the Storage Access API: an
embedded frame can ask the browser, on a user gesture, for access to its own
unpartitioned first-party cookies, and both Safari and Chrome implement it.
That would read the cookie the callback already set with no backend work, at
the cost of a revocable permission prompt. It belongs in the comparison, and it
is the only one of the three routes that is not a backend feature.

And the constraint bullet still offered CHIPS or a token not relying on ambient
cookies as alternatives, when the header path relies on one as soon as the page
reloads. Both routes rest on the same attribute; they differ over who writes the
cookie and what that costs.
This commit is contained in:
Dmitry Verkhoturov
2026-08-24 01:09:05 +01:00
parent be61b5873a
commit 3b7a7778dc
@@ -39,8 +39,8 @@ cookies. Nothing short of the reload proves it: the widget holds a token in memo
page, so a test that signs in and posts without reloading passes while the persistence is entirely
broken.
OAuth sits outside that criterion as the flow is built today, and no frontend change brings it
inside. The provider callback is a top-level navigation in a popup, so `Service.Set` writes its
OAuth sits outside that criterion as the flow is built today. The provider callback is a top-level
navigation in a popup, so `Service.Set` writes its
cookie there as an ordinary first-party cookie for the auth host, carrying no `Partitioned` and so
having no partition key at all. The frame on `food.com` sees it only as an unpartitioned third-party
cookie, which is exactly what a blocking browser refuses. The cookie is `HttpOnly` besides, so the
@@ -48,14 +48,26 @@ popup cannot read it and hand it over in script. `SameSite` is not what obstruct
separate-domain manual has operators set `AUTH_SAME_SITE=none`, and the callback navigation is
top-level regardless.
Bringing OAuth inside the criterion needs a server-mediated handoff, where the popup posts a
one-time code to its opener and the frame redeems it over XHR. Two shapes exist and they cost
differently. If the redemption answers with `Set-Cookie`, that cookie has to carry `Partitioned` to
Two routes could bring OAuth inside the criterion, and both need costing before either is called
mandatory.
The first is the Storage Access API, which exists for exactly this shape: an embedded frame asks
the browser, with a user gesture, for access to its own unpartitioned first-party cookies, and
Safari and Chrome both implement it. That would let the frame read the cookie the callback already
set, with no backend work at all. It is not free, since it spends a permission prompt and the
grant is revocable and scoped, but it is a real option and this section previously wrote it off by
claiming no frontend change could help.
The second is a server-mediated handoff, where the popup posts a one-time code to its opener and
the frame redeems it over XHR. Two shapes exist there and they cost differently. If the redemption
answers with `Set-Cookie`, that cookie has to carry `Partitioned` to
be stored at all, which is the upstream library work described below. If it answers with `X-JWT`
instead, `fetcher.ts` writes the partitioned pair itself and nothing upstream has to change, which
makes it the cheaper of the two. Either is a backend feature and out of scope here. Until one
exists, an operator who needs OAuth off-domain serves remark42 from the same registrable domain as
the site, or accepts that OAuth readers sign in on the instance's own origin.
makes it the cheaper of the two. Both handoff shapes are backend features and out of scope here; the
Storage Access
route is not, and is the one worth pricing first for that reason. Until something lands, an
operator who needs OAuth off-domain serves remark42 from the same registrable domain as the site,
or accepts that OAuth readers sign in on the instance's own origin.
**Where that stands.** Master satisfies the criterion today, provided `AUTH_SEND_JWT_HEADER` is on.
The server returns the token in `X-JWT` and `fetcher.ts` writes the `JWT` and `XSRF-TOKEN` cookies
@@ -131,8 +143,12 @@ Constraints on any frontend design:
outright and honours only cookies explicitly marked `Partitioned`; its CHIPS support has been
switched on, off and on again across releases, so pin the current state before relying on a
version number. A design depending on `SameSite=None` surviving has an expiry date
- the endpoint is CHIPS (`SameSite=None; Secure; Partitioned`) or a token not relying on ambient
cookies. Neither is a flag flip, and the first cost is upstream rather than here:
- the endpoint is CHIPS (`SameSite=None; Secure; Partitioned`) either way, and the only question is
who writes the cookie. Calling the header path a token that does not rely on ambient cookies was
wrong: it holds the token in memory for one page, and after a reload the browser sends the
partitioned cookie the frontend wrote, so both routes stand on the same attribute and differ only
in whether the server or the frontend sets it, plus the XSS cost the frontend writer carries.
Neither is a flag flip, and the first cost is upstream rather than here:
`go-pkgz/auth/v2@v2.2.0` cannot emit `Partitioned` at all. Both cookies are hand-built in
`Service.Set` with only `HttpOnly`, `Path`, `Domain`, `MaxAge`, `Secure` and `SameSite`, and
`AUTH_SAME_SITE` in `cmd/server.go` offers `default`/`none`/`lax`/`strict` with no partitioned