Define Home and team entry integration stages
1 file changed,
+41
−0
implementation/team-entry.md+41 −0
CI passed — run details
Intent
diff --git a/implementation/team-entry.md b/implementation/team-entry.md| new file mode 100644 |
| index 0000000..8d3edb2 |
| --- /dev/null |
| +++ b/implementation/team-entry.md |
| @@ -0,0 +1,41 @@ |
| +# Home and team app entry |
| + |
| +Status: implementation proposal, 2026-09-08. The user has prioritized docs.oficina.works → Home sign-in → team choice → team Docs, and accepted the existing team-subdomain shape. This branch starts with the shared Home/directory exchange; the acceptance target still includes Docs and two teams. Copy review is waived for this round. Fable reviews each implementation stage. |
| + |
| +## Deployment and authority |
| + |
| +The credential origin remains id.oficina.works and the directory remains oficina.works. Team instances use <team>.docs.oficina.works. The common docs host becomes an entry point only after the existing production Docs instance has a verified team alias and all existing links still resolve. No replacement of the production database or keys is part of this work. |
| + |
| +The Home binary runs in two explicitly configured roles, credential and directory, with separate processes, databases and host-only session cookies. Each role refuses the other origin's routes even when a caller supplies its Host header. The credential role has the assertion private key; the directory has only pinned public verification keys. Existing sign-in secrets stay with their existing databases. The directory never obtains a credential session, and the credential origin never renders a team label or account-authored string. |
| + |
| +For the first isolated pilot, the operator supplies a bounded immutable consumer manifest through deployment configuration. Each consumer has an opaque ID, exact canonical HTTPS origin and fixed callback path /auth/assertion. No request can register or alter a consumer. The directory is a consumer too. Consumer registration in the eventual production provisioning/drain path remains explicit; pilot bootstrap is not a web administration endpoint. |
| + |
| +The new rastrillo assertion package signs and verifies the opaque person_ref, exact origins, independent jti and consumer nonce, issued/expiry/authentication time. P-256 reuses crypto.MarshalKeypair for the credential-only signing secret. Max lifetime is 120 seconds. Verification keys are pinned for this first deployment; no token-driven key fetch. A rotation deploys old+new public keys before switching the signer, then removes the old key after the handoff horizon. No email, role, membership, team authority or resource path rides in an assertion. |
| + |
| +## 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. |
| +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. |
| +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, seal the received token into a two-minute host-only HttpOnly handoff cookie, and 303 to the consumer's fixed GET /auth/finish. No session is minted yet. That top-level GET carries the consumer's pending cookie. Read and clear the handoff cookie, 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 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. |
| +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 |
| + |
| +The directory owns teams, authoritative memberships and team instance routes. Choose only among the signed-in person's active memberships that have the requested app provisioned; one team redirects automatically, several render a shared Estilo choice page, none render an empty state. Only server-owned origins enter redirect targets. POST choice checks current membership again. Persisting a last choice may change the default, never access. |
| + |
| +Call 8 uses a consumer token scoped to one team. A complete roster includes generation and epoch and is paged with epoch-bound cursors. Apps stage all pages and publish one complete roster atomically, including an explicitly complete empty roster. Stale or failed refresh never extends the maximum age. Every request, including an existing session, loses team-standing access when the roster becomes stale; existing individual grants remain their own authority. A restored Home generation requires directory-owner confirmation before consumers trust it. Roster timing and recovery tests precede its deployment. |
| + |
| +Existing Docs people are keyed by verified email, while Home uses person_ref. Add a stable (issuer, subject) → local PersonID mapping. Link an existing person only after proving both an existing Docs sign-in and the Home identity in one browser-bound transaction. Never infer a mapping by email from an assertion, generate a fake email, or replace document ownership IDs. New Home-only people need a nullable legacy-address field or separate identity model and a forward migration preserving the existing unique addresses. Existing address grants continue to resolve through a linked verified legacy identity. Home membership itself grants no access to existing private documents. |
| + |
| +The common app host has no authority over team content. Its GET routes initiate directory selection and preserve bounded navigation intent. Existing production document links must continue to resolve through the original team's verified alias; a deployment must not redirect every old link to whichever team the user most recently selected. Exact behavior for this compatibility entry is reviewed before the common host changes role. |
| + |
| +## Release evidence |
| + |
| +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. |