Correct R1 in the frontend plan: scope it to the flows that can meet it
The acceptance criteria promised sign-in with any configured provider under third-party cookie blocking, which OAuth as built cannot satisfy: the callback runs in a popup, a top-level context of its own, so the cookie it sets is keyed to the auth host and the frame embedded on the other domain is a different partition that never sees it. The criteria now name email, Telegram and anonymous, and OAuth carries its own paragraph explaining why it is out and what would bring it in, which is the server-mediated one-time code the constraints below already describe. Two claims went stale alongside it. Master meets the criterion today with AUTH_SEND_JWT_HEADER on, because fetcher.ts writes the cookie from inside the frame and the token never travels as a third-party cookie, so the remaining gap for those flows is documentation and not code. The server-set cookies still lack Partitioned, but nothing depends on them surviving in a third-party frame any more, so the upstream work they were waiting on buys no flow that does not already work.
This commit is contained in:
@@ -33,23 +33,44 @@ users report against most. The target arrangement is remark42 serving from its o
|
||||
documented in `site/content/docs/manuals/separate-domain/index.md`, so operators follow it and then
|
||||
find that authentication behaves differently from the same-domain case.
|
||||
|
||||
**Acceptance criteria.** A reader on `food.com` can sign in with any configured provider, post,
|
||||
reload the page, and still be signed in, on a browser that blocks unpartitioned third-party cookies.
|
||||
Nothing short of the reload proves it: the widget holds a token in memory for the life of a page, so
|
||||
a test that signs in and posts without reloading passes while the persistence is entirely broken.
|
||||
**Acceptance criteria.** A reader on `food.com` signs in through email, Telegram or anonymous,
|
||||
posts, reloads the page, and is still signed in, on a browser that blocks unpartitioned third-party
|
||||
cookies. Nothing short of the reload proves it: the widget holds a token in memory for the life of a
|
||||
page, so a test that signs in and posts without reloading passes while the persistence is entirely
|
||||
broken.
|
||||
|
||||
**Where that stands.** The e2e suite covers the rendering half through
|
||||
`TestCrossOrigin_WidgetRendersOnAnotherOrigin`, which proves the document loads on another origin
|
||||
and reports itself through postMessage across the boundary, and `ALLOWED_HOSTS` refusal through
|
||||
`TestCrossOrigin_DisallowedHostNeverReportsInited`. The authentication half is not covered and
|
||||
cannot be until the e2e stack speaks https, because an embedded cookie needs `SameSite=None`, which
|
||||
browsers accept only with `Secure`.
|
||||
OAuth sits outside that criterion as the flow is built today, and no frontend change brings it
|
||||
inside. The provider callback lands in a popup, which is a top-level context of its own, so the
|
||||
cookie set there is keyed to the auth host and not to `food.com`, and the frame embedded in
|
||||
`food.com` is a different partition that never sees it. `SameSite` compounds it, being judged
|
||||
against the top-level site rather than against the request's own origin, and the cookie is
|
||||
`HttpOnly` besides, so the popup cannot read it to hand it over. None of that is a defect; it is the
|
||||
partitioning model behaving as specified. Bringing OAuth inside the criterion needs the
|
||||
server-mediated handoff described in the constraints below, where the popup posts a one-time code
|
||||
back to its opener, the frame redeems it over XHR, and the `Set-Cookie` answering that request is
|
||||
issued to the embedded context and keyed to `food.com`. That is a backend feature and out of scope
|
||||
here. Until it 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.
|
||||
|
||||
`ALLOWED_HOSTS` sets the CSP `frame-ancestors` and `AUTH_SAME_SITE=none` lets auth cookies be set
|
||||
from any embedding domain. OAuth is what visibly fails off-domain (discussion #1139). Telegram,
|
||||
email and anonymous work where third-party cookies are still permitted, and stop working where they
|
||||
are not: the server's cookies carry no `Partitioned`, and the header fallback is off by default, so
|
||||
nothing saves them once the browser blocks unpartitioned third-party cookies.
|
||||
**Where that stands.** Master satisfies the criterion today, provided `AUTH_SEND_JWT_HEADER` is on.
|
||||
The server returns the token in `X-JWT`, `fetcher.ts` writes the `JWT` and `XSRF-TOKEN` cookies
|
||||
itself, and because that write runs inside the frame the pair is keyed to `food.com` and comes back
|
||||
on the reload. The token never travels as a third-party cookie, which is why the browser's blocking
|
||||
never reaches it. The flag is off by default and `site/content/docs/manuals/separate-domain/index.md`
|
||||
does not mention it, so the arrangement works and nobody is told how; documenting it is the whole
|
||||
remaining gap for the flows named above.
|
||||
|
||||
The e2e suite covers the rendering half through `TestCrossOrigin_WidgetRendersOnAnotherOrigin`, which
|
||||
proves the document loads on another origin and reports itself through postMessage across the
|
||||
boundary, and `ALLOWED_HOSTS` refusal through `TestCrossOrigin_DisallowedHostNeverReportsInited`.
|
||||
#2214 adds the authentication half over TLS, the reload included, and runs it once more against a
|
||||
browser configured to block third-party cookies. That last case needs `IgnoreDefaultArgs`, because
|
||||
Playwright's own `--disable-features` list switches partitioning off and beats the flags passed
|
||||
through `Args`, which would leave the case asserting nothing.
|
||||
|
||||
`ALLOWED_HOSTS` sets the CSP `frame-ancestors` and `AUTH_SAME_SITE=none` lets the server's auth
|
||||
cookies be set from any embedding domain. Those server-set cookies carry no `Partitioned`, so they
|
||||
are the ones the browser drops; what survives the drop is the header fallback above.
|
||||
|
||||
There is a second mechanism aimed squarely at this, `AUTH_SEND_JWT_HEADER`, which returns the token
|
||||
in a response header so the client can present it without relying on an ambient cookie. #1877 was
|
||||
@@ -58,8 +79,9 @@ first half. Its persistence never worked on https: the client wrote its copy und
|
||||
prefix that neither the backend nor the widget's own reader ever asks for, and marked it
|
||||
`SameSite=Strict`, which is never sent from a third-party frame. Both are corrected here, and the
|
||||
frontend now marks its cookies `SameSite=None; Secure; Partitioned` when it detects a third-party
|
||||
context. The server-set cookies still need the same treatment, and that is upstream work in
|
||||
`go-pkgz/auth`.
|
||||
context. The server-set cookies are untouched and still carry no `Partitioned`, but with the header
|
||||
path in place nothing depends on them surviving in a third-party frame, so partitioning them upstream
|
||||
buys no flow that does not already work.
|
||||
|
||||
Constraints on any frontend design:
|
||||
|
||||
@@ -75,8 +97,9 @@ Constraints on any frontend design:
|
||||
`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
|
||||
axis, since `Partitioned` is a separate attribute. That is a PR to the library before anything in
|
||||
this repo changes
|
||||
axis, since `Partitioned` is a separate attribute. The first option therefore starts with a PR to
|
||||
the library; the second needs nothing upstream and is already shipped, which is why it is the one
|
||||
R1 leans on
|
||||
- the popup-to-iframe handoff cannot be done in JS. The JWT cookie is `HttpOnly: true`, set in
|
||||
`Service.Set`, so the top-level popup cannot read it to hand over, and the receiving end would not
|
||||
work either: `setAuthCookie` in `cookies.ts` writes `SameSite: 'Strict'` under a `__Host-` prefix
|
||||
@@ -460,7 +483,8 @@ compiled, it is what verifies it.
|
||||
**Cost**: months to an opt-in parallel UI reads as a floor derived from the optimistic architecture,
|
||||
and the optimistic architecture does not hold. Anonymous-first is forced rather than chosen, so the
|
||||
fragment layer reproduces the entire authenticated tree rather than a delta; add the three
|
||||
subscription flows and, if R1 is to be honoured, the upstream `Partitioned` work in `go-pkgz/auth`.
|
||||
subscription flows and, if R1 is to be honoured, a server-side equivalent of the header fallback,
|
||||
since a server-rendered fragment has no client-side writer to key the cookie to the embedding site.
|
||||
On one contributor the realistic figure is long enough that the plan's own warning applies to the
|
||||
schedule and not only to the design. Deletes the entire npm toolchain. **Against it**: 2,400 lines
|
||||
of the most stateful code get rewritten; a second HTML-fragment API surface becomes permanent
|
||||
|
||||
Reference in New Issue
Block a user