Bound handoff transport with a sealed server inbox
1 file changed,
+2
−2
implementation/team-entry.md+2 −2
CI passed — run details
Intent
diff --git a/implementation/team-entry.md b/implementation/team-entry.md| index 8d3edb2..8cc580e 100644 |
| --- a/implementation/team-entry.md |
| +++ b/implementation/team-entry.md |
| @@ -19,8 +19,8 @@ The new rastrillo assertion package signs and verifies the opaque person_ref, ex |
| 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. |
| +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. |
| +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. |
| 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. |