Cross-Host Session Handoff — Threat Model
logi uses a host=role separation architecture. api.1pass.dev (end-user) and start.1pass.dev (developer/account) do not share cookie scope (we cannot widen the cookie domain to .1pass.dev because of the embed.1pass.dev iframe isolation). To use the same session, authenticated, on both hosts, we mint a per-host cookie via a single-use handoff token (SessionHandoffToken).
The policy in one line
| Host | Required role |
|---|---|
| api.1pass.dev | user |
| start.1pass.dev | user (relaxed 2026-05-19; previously developer) |
→ HandoffPolicy::REQUIRED_ROLES (app/policies/handoff_policy.rb).
Why we relaxed it
/account is the self-service entry point for ordinary users, yet it is host-locked to start.1pass.dev. If the policy allowed only developer, an ordinary user who visited /session/new directly and signed in could not reach /account → a dead-end UX.
Why it is safe — each controller carries its own gate
Relaxing the policy means an ordinary user can create a start.1pass.dev cookie, but that only means "they can reach the start host." Sensitive surfaces carry their own gates:
| surface | gate |
|---|---|
/developer/* | Developer::BaseController#require_developer_or_admin |
/console/applications, etc. | same |
/cli/auth/start, /cli/auth/approve | Cli::AuthorizationsController#require_developer_or_admin (added 2026-05-19, per a codex BLOCKER) |
/account/* | users access only their own account area — no gate (intended behavior) |
Mandatory checklist when adding a new surface
When you put a new route/controller on start.1pass.dev, you must verify:
- Is it a sensitive operation? (RP registration, secret issuance, admin privileges, etc.) → add
require_developer_or_admin(or a stricter gate) to the controller. - Does it expose PII? → add a
Current.usermatch check so only the owner can access it. - Is it a privilege-escalation surface like CLI scope issuance? → require developer + add protection such as
RequireRecentOtp.
If a gate is missing, an ordinary user can reach the start host with a start cookie and then escalate privileges → a privilege-escalation vulnerability. The codex review (2026-05-19) first caught this (the case where /cli/auth/start could issue apps:manage with no gate).
SessionHandoffToken's own invariants
TTL 30s— the token expires if it is not redeemed within 30 seconds of issuance.- Single-use enforced by the
consumed_atcolumn (consume!is an atomic update). - The
target_hostfield pins the host at mint time. Redeeming on a different host makesconsume!return nil (a codex post-merge fix). An attempt to redeem an API token on start is rejected. - The mint side runs
cookies.delete(:session_id)— the token and the cookie are never alive at the same time for the same session (a single-auth-artifact policy).
Threat 1 — token theft + reuse
Scenario: an attacker intercepts the handoff URL (/session/handoff?token=...).
Mitigation:
- TTL 30 seconds — even if intercepted, it expires quickly.
- Single-use — once the legitimate user redeems the token, it is dead (replay blocked).
- target_host binding — cannot be diverted to another host.
- HTTPS enforced (production) — cleartext interception blocked.
Threat 2 — open redirect via return_to
Scenario: can /session/handoff?token=...&return_to=https://attacker.com send the user to an arbitrary URL?
Mitigation: CrossHostHandoff.safe_path? admits only a path-only value on the per-host allowlist. External URLs, protocol-relative URLs, and prefix traps (/accountXYZ) are all rejected → it redirects to the fallback path instead.
Regression spec: spec/requests/session_handoffs_spec.rb#"failure modes".
Threat 3 — privilege escalation (calling a sensitive surface after entering the start host)
Scenario: an ordinary user obtains a start.1pass.dev cookie via handoff → calls /cli/auth/start?...&scope=apps:manage.
Mitigation: see the "gate checklist" above. Each controller verifies Current.user.developer? itself. The policy relaxation only permits host entry; calling a power surface requires a separate gate.
Regression specs:
spec/policies/handoff_policy_spec.rbspec/requests/cli/authorizations_spec.rb#"rejects regular user"
Related code
app/policies/handoff_policy.rbapp/controllers/session_handoffs_controller.rbapp/controllers/api/v1/me/cross_host_handoffs_controller.rbapp/models/session_handoff_token.rbconfig/initializers/cross_host_handoff.rb