Repair two sentences the previous fix broke, and finish one it missed

Verifying the last pass found that two of its own corrections introduced fresh
errors. Removing a reference to an earlier draft left the Telegram sentence
circular, comparing a defect to a milder form of itself with no antecedent. And
the OAuth scope sentence attached its relative clause to the wrong thing, so it
read as though blocking-or-partitioning were the recommended recipe; those are
different facts, since the recipe drops AUTH_SAME_SITE=none and the callback
cookie then carries no attribute at all.

One correction had not been applied. The line saying AUTH_SAME_SITE=none adds an
unpartitioned HttpOnly JWT and contributes nothing was true of the engine the
cookie jar was read on and stated of all of them; Safari drops that pair
outright and Firefox delivers it either way, so it is dead weight on all three
for three different reasons.

Smaller: a configuration survives a browser setting instead of reaching it, the
Path B task list is above its cost paragraph and not below, Firefox never holds
a partitioned copy instead of not needing one, and the two remaining shorthand
mentions of XSS exposure now name the token exposure they mean.
This commit is contained in:
Dmitry Verkhoturov
2026-08-24 01:50:13 +01:00
parent d5ede91a9f
commit 5045d69c20
@@ -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.