diff --git a/docs/plans/2026-08-19-frontend-simplification.md b/docs/plans/2026-08-19-frontend-simplification.md index 169e21fe..bad79a59 100644 --- a/docs/plans/2026-08-19-frontend-simplification.md +++ b/docs/plans/2026-08-19-frontend-simplification.md @@ -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