diff --git a/docs/plans/2026-08-19-frontend-simplification.md b/docs/plans/2026-08-19-frontend-simplification.md index 4ad983de..124b7f0f 100644 --- a/docs/plans/2026-08-19-frontend-simplification.md +++ b/docs/plans/2026-08-19-frontend-simplification.md @@ -42,15 +42,15 @@ broken. **Two things about this criterion are decisions, not findings, and they belong to the maintainer.** They are marked so neither is settled by implication. -The first is that it excludes OAuth, where the requirement as originally written said any -configured provider. That is a narrowing of product scope, not a correction of fact. OAuth -off-domain fails for the flow as built on any browser that blocks or partitions third-party -cookies, which is Safari's default and the recommended recipe everywhere, but the routes priced -below would change that, and -the narrowing sits awkwardly beside `fixing #1139 must not get harder` in the Path B constraints, -which is the same subject. Either the requirement excludes OAuth and #1139 is a separate goal -carrying its own timeline, or the requirement keeps OAuth and is not met today. This document -assumes the first and is not entitled to assume it. +The first is that it excludes OAuth, where the requirement as originally written said any configured +provider. That is a narrowing of product scope, not a correction of fact. OAuth off-domain fails for +the flow as built on any browser that blocks or partitions third-party cookies, which is Safari's +default; under the recommended recipe, which drops `AUTH_SAME_SITE=none`, it fails on the remaining +browsers too. The routes priced below would change that, and the narrowing sits awkwardly beside +`fixing #1139 must not get harder` in the Path B constraints, which is the same subject. Either the +requirement excludes OAuth and #1139 is a separate goal carrying its own timeline, or the +requirement keeps OAuth and is not met today. This document assumes the first and is not entitled to +assume it. The second is what counts as evidence, and the criterion is not met end to end by any single run. The criterion asks for sign-in, a post and a reload. The permanent suite covers sign-in, post and @@ -111,7 +111,7 @@ or accepts that OAuth readers sign in on the instance's own origin. **Where that stands.** Master satisfies the criterion today on every browser measured except one, provided `AUTH_SEND_JWT_HEADER` is on. The exception is Firefox with "block all third-party cookies" -chosen, which discards partitioned cookies too and which no configuration reaches. +chosen, which discards partitioned cookies too and which no configuration survives. The server returns the token in `X-JWT` and `fetcher.ts` writes the `JWT` and `XSRF-TOKEN` cookies itself through `authCookieOptions`, which marks them `SameSite=None; Secure; Partitioned` in a third-party context. The attribute is what carries the reload, not the fact that the write happened @@ -168,9 +168,10 @@ The documentation gap this section named is closed by #2218. The separate-domain recommends `AUTH_SEND_JWT_HEADER=true` and carries the XSS trade-off in the same breath, and the parameter page no longer promises `SameSite=Strict` cookies and a `__Host-` prefix that `authCookieOptions` stopped emitting in a third-party context at #2197. That PR also measured -`AUTH_SAME_SITE=none` out of the recommended recipe: with the header flag on it adds an -unpartitioned `HttpOnly` JWT to third-party delivery and contributes nothing to persistence, which -the cookie jar shows directly. +`AUTH_SAME_SITE=none` out of the recommended recipe: on Chromium, where that jar was read, the +header flag leaves it adding an unpartitioned `HttpOnly` JWT to third-party delivery and +contributing nothing to persistence. Safari drops that pair outright and Firefox delivers it either +way, so the setting is dead weight on all three for different reasons. 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 @@ -189,13 +190,12 @@ exercised nowhere and cannot be until #2208 makes the Telegram API base URL conf without that the stack cannot answer as Telegram; `go-pkgz/auth` #316 is the change that would let the suite measure it. The inference itself is that the client-side writer keys off `X-JWT` on any auth response and not off the provider, so nothing in it distinguishes one flow from another. The -distinction is worth keeping visible, because a criterion resting on flows the suite cannot reach is -a milder version of the same defect in milder form: a requirement resting on what the suite cannot -reach. +distinction is worth keeping visible, because a criterion resting on flows the suite cannot reach +asserts more than the evidence holds, which is the defect this section exists to avoid. `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 Chromium, WebKit and Safari drop; Firefox keeps them, which is why it needs no +are the ones Chromium, WebKit and Safari drop; Firefox keeps them, which is why it never holds a partitioned copy of its own. 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 @@ -209,7 +209,7 @@ context. The server-set cookies are untouched and still carry no `Partitioned`. the upstream work matter, not what excuses it: `AUTH_SEND_JWT_HEADER` defaults to false, so in the configuration remark42 actually ships nothing writes a partitioned cookie anywhere, and partitioning the server's pair is what would carry email, anonymous and Telegram off-domain without asking -operators for a flag that costs them XSS exposure. +operators for a flag that exposes the token to script. Constraints on any frontend design: @@ -231,7 +231,8 @@ Constraints on any frontend design: `AUTH_SAME_SITE` in `cmd/server.go` offers `default`/`none`/`lax`/`strict` with no partitioned axis, since `Partitioned` is a separate attribute. The first option therefore starts with a PR to the library. The second needs nothing upstream and already works, which is why R1 leans on it, but - it is gated behind a flag that ships off and costs XSS exposure, so it answers the requirement + it is gated behind a flag that ships off and exposes the token to script, so it answers the + requirement without being the answer an operator gets by default - 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. The receiving end is no longer @@ -640,7 +641,7 @@ 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. R1 costs Path B little: the task list below retains the existing auth through +subscription flows. R1 costs Path B little: the task list above retains the existing auth through `htmx:configRequest`, and `request` in `fetcher.ts` already turns `X-JWT` into the partitioned pair, so Path B can keep that writer or attach the same handling to a response hook.