oficina / home Public

Resolve browser transport review findings

Paul Campbell pushed by paul@keymail.dev 36f876c647f50162aa871e6012a42627a0d27eab parent d116692
1 file changed, +13 −5
  • implementation/team-entry.md +13 −5

CI passed — run details

diff --git a/implementation/team-entry.md b/implementation/team-entry.md
index 8cc580e..14cfa94 100644
--- a/implementation/team-entry.md
+++ b/implementation/team-entry.md
@@ -14,14 +14,14 @@ The new rastrillo assertion package signs and verifies the opaque person_ref, ex
## Browser exchange
-1. A consumer's GET /auth/start selects a validated local return path (bounded, no scheme/authority/control characters/backslashes, no recursive auth routes). It generates independent random browser secret and signed nonce. Store only their hashes with purpose, local return path and ten-minute expiry in the consumer database. Return paths containing sensitive resource identifiers are sealed before storage. The __Host- pending cookie is Secure, HttpOnly, Path=/, SameSite=Lax and has no Domain. Duplicate cookies are refused. One pending request per browser is the initial bound: starting another in the same consumer replaces the previous pending request. Different team origins remain independent.
+1. A consumer's GET /auth/start selects a validated local return path (bounded, no scheme/authority/control characters/backslashes, no recursive auth routes). It generates independent random browser secret and signed nonce. Store only their hashes with purpose, local return path and ten-minute expiry in the consumer database. Every local return path is sealed before storage. The maximum path+query length is 2048 bytes; a query string is permitted for existing ?team= navigation. The __Host- pending cookie is Secure, HttpOnly, Path=/, SameSite=Lax and has no Domain. Duplicate cookies are refused. One pending request per browser is the initial bound: starting another in the same consumer replaces the previous pending request. Different team origins remain independent.
2. Redirect to the configured credential origin /authorize with only registered consumer ID and nonce. These are request identifiers, not signed assertions or identity. Strict query parsing rejects duplicates and unknown keys. The credential origin validates the registered consumer and nonce before doing anything. It retains the authorization request in a sealed, ten-minute __Host- intent cookie. No arbitrary return URL is accepted. Signing in continues to the fixed /authorize/resume route through the existing sign-in return machinery, including second factor and recovery.
-3. Only a complete credential session may mint. The original AuthTime (or At when absent) is retained. The issuer makes a fresh jti and a 120-second assertion for exactly that consumer origin and nonce. A handoff page posts it to the registered callback. The page contains no account-authored visible string, uses no-store and no-referrer, and permits form-action only self plus that exact registered origin.
-4. The automatic handoff needs one narrowly allowed static same-origin script that submits this form; it has a visible Continue button when JavaScript is off. This is a proposed extension of the existing Home script allowlist for the user's automatic-return flow, not an attribution to the older passkey-only ruling. All other credential pages keep their existing script restrictions. No inline script or arbitrary form target is allowed.
+3. Only a complete credential session may mint. The original AuthTime (or At when absent) is retained. The issuer makes a fresh jti and a 120-second assertion for exactly that consumer origin and nonce. A handoff page posts it to the registered callback. The page contains no account-authored visible string, uses no-store and strict-origin-when-cross-origin, and uses the process-wide CSP with form-action self plus every exact manifest origin (including the directory). The handoff URL carries no identity or signed token. Never use no-referrer here: browsers may send Origin: null on the cross-origin POST.
+4. The automatic handoff needs one narrowly allowed static same-origin script that submits this form; it has a visible Continue button when JavaScript is off. This is an implementation decision under the user's explicit approval of the automatic-return flow in this session, not an attribution to the older passkey-only ruling. Record it as that current authorization when amending the spec and the exact template/path script allowlist. All other credential pages keep their existing script restrictions. No inline script or arbitrary form target is allowed.
5. A callback accepts only POST application/x-www-form-urlencoded with a bounded 12 KiB body, exactly one assertion field and the exact configured issuer Origin. Mount this route outside ordinary same-origin CSRF middleware only; all other mutations retain that middleware. Reject a foreign Origin even when its Sec-Fetch-Site says same-site. No CORS is added.
-6. SameSite=Lax cookies may be absent on a cross-site POST, so the callback must not depend on receiving them. Verify the assertion, write it sealed into a two-minute consumer inbox row keyed by the hash of a fresh random 32-byte handle, set that handle in a host-only HttpOnly handoff cookie, and 303 to the consumer's fixed GET /auth/finish. No session is minted yet. Prune expired inbox/pending rows on each write and boot; cap each table at 10,000 live rows, refusing new starts/callbacks when full. The cap bounds unauthenticated writes without evicting an active login. Tokens stay in sealed storage, never in a cookie large enough to hit browser size limits. That top-level GET carries the consumer's pending cookie. Read and clear the handoff cookie, load its sealed inbox row by handle hash, reverify the assertion against the current clock, compare the signed nonce with the pending request using constant-time equality, and compare the browser secret hash. An attacker may overwrite a handoff cookie only to cause a failed login: a different browser's pending state never matches.
+6. SameSite=Lax cookies may be absent on a cross-site POST, so the callback must not depend on receiving them. Verify the assertion, write it sealed into a two-minute consumer inbox row keyed by the hash of a fresh random 32-byte handle, set that handle in a __Host- handoff cookie with Secure, HttpOnly, Path=/, SameSite=Lax and no Domain, and 303 to the consumer's fixed GET /auth/finish. No session is minted yet. Prune expired inbox/pending rows on each write and boot; cap each table at 10,000 live rows. Evict the oldest pending row to admit a new start when full; this discards only unauthenticated navigation intent and requires that browser to restart. Refuse callbacks if the inbox is full. Tokens stay in sealed storage, never in a cookie large enough to hit browser size limits. Inbox has a unique (issuer, jti-hash) index, so replaying a callback cannot allocate another row. That top-level GET carries the consumer's pending cookie. Read and clear the handoff cookie, load its sealed inbox row by handle hash, reverify the assertion against the current clock, compare the signed nonce with the pending request using constant-time equality, and compare the browser secret hash. An attacker may overwrite a handoff cookie only to cause a failed login: a different browser's pending state never matches.
7. Atomically delete the live pending request and its inbox row and insert (issuer, jti, expiry) into the replay table before issuing a session. Duplicate/concurrent submission has one winner. On a storage failure, refuse. A later session-write failure consumes the assertion; the user starts again. Never roll back replay consumption after a session cookie may have been sent. Existing identity is not silently switched by a handoff: an already signed-in consumer with a different subject refuses and requires explicit sign-out first.
-8. Replay pruning requires a durable clock fence. Track the maximum observed wall time and maximum accepted assertion expiry in the same transaction. Refuse exchanges after clock rollback below that wall-time fence; do not prune or mint while fenced. On normal operation, delete expired entries only after advancing the fence transactionally. A process restart uses the stored fence. A restored consumer database requires draining and an explicit reset of assertion trust/pending state before accepting new traffic; deployment must not claim arbitrary snapshot rollback is replay safe.
+8. Replay pruning requires a durable clock fence. Track the maximum observed wall time and maximum accepted assertion expiry in the same transaction. Refuse exchanges after clock rollback more than 30 seconds below that wall-time fence; do not prune or mint while fenced. Retain replay rows through expiry plus 30 seconds so a tolerated backward clock adjustment cannot resurrect a pruned token. On normal operation, delete entries whose expiry plus 30 seconds precedes the stored wall-time fence, after advancing that fence transactionally. All comparisons are overflow-safe. A process restart uses the stored fence. A restored consumer database requires draining and an explicit reset of assertion trust/pending state before accepting new traffic; deployment must not claim arbitrary snapshot rollback is replay safe.
9. Issue a separate consumer session with the verified subject and original AuthTime, then redirect to the stored local path. The directory session proves identity only. Every team choice and app request still needs membership or an existing individual grant.
## Directory and Docs stages
@@ -39,3 +39,11 @@ The common app host has no authority over team content. Its GET routes initiate
First exercise the exchange between isolated credential/directory origins with real HTTPS, Chromium and WebKit, JavaScript on/off. Test foreign origin, missing/duplicate cookies, wrong nonce, wrong audience, expired/reused tokens, parallel callbacks, stale auth_time, second-factor continuation, clock rollback/restart and no token in URLs/logs. Existing Home privacy and script gates are extended rather than replaced.
Then run two isolated Docs teams: one-team automatic choice, multiple-team selection, direct resource return, separate tabs for each team, membership removal and failed roster refresh, account linking preserving existing ownership and grants, sign-out and session expiry. Full app CI, Fable review and verified canary versions precede production. Notify the user after every verified deployment with the exact URL and what to try; a library merge is not a deployed sign-in flow.
+
+## Review corrections and implementation choices
+
+Fable reviewed d116692 and found a functional referrer-policy bug, missing explicit cookie/CSP details and write amplification. The revised protocol above fixes each. Two-process topology is the already deployed preview shape, now made explicit and enforced by role; each database needs its own privacy and restore checks. The directory loader requires only its sealing key and pinned verification keys, never credential mail, pepper or signing material. Pilot immutable registration stays scoped to isolated canaries; it is not a completed production provisioning design. Oldest-pending eviction is the chosen bounded-cache policy.
+
+Update the Home spec in the implementation commit: §4/§10 describe the explicit role topology; §5.3 records P-256, signed nonce/auth_time and pinned pilot keys; claims omit optional prefs and roster_epoch in this bounded stage rather than falsely claiming those consumers are implemented. Preserve their later contracts. Amend script ruling 21 with the current user's approved automatic-return requirement and the narrowly chosen static handoff enhancement; label the implementation decision honestly, not as a verbatim historical ruling. Extend scriptAllowed with exactly the handoff template and static path, credentialReturn with /authorize/resume, and existing privacy/log gates with sealed inbox/pending, replay, directory session and known assertion-byte canaries.
+
+Reuse the consumer exchange across Home and Docs rather than implementing two subtly different callbacks. The framework's pure core approval does not approve this adapter: its storage/HTTP implementation will have its own tests and review. A shared consumer accepts the application's database handle, sealer and session-issuance callback; it does not hold memberships or item grants. Keep legacy app authentication working while adding opaque Home subject mapping; do not require a full framework/UI migration merely to import the pure assertion primitive.