Skip to content

RFC: replace static bearer token with device-session auth (pairing, HttpOnly cookies, managed API tokens) #85

Description

@ashwin-pc

Motivation

pi-web authenticates everything with one static bearer token (PI_WEB_TOKEN). One secret doing every job badly:

  • Static: never rotates, no expiry; revocation = re-key every device.
  • In URLs: WebSocket connects with ?token= (src/app/api.ts wsUrl()), initial page load accepts ?token=, and the QR share hands out the permanent token — history/log leakage by design.
  • In localStorage (pi-web-token): readable by any same-origin script.
  • Cannot attach to browser-native loads (<img>, <video>, <iframe src>, downloads) — which is why /api/artifacts/ is served before the auth gate (server.tsserveArtifact runs ahead of isAuthorized). Consequence: anyone with network reach can read .pi/web/artifacts/** of known cwds without the token. With tunneling in the product, that is a real exposure.
  • One credential for all devices: no per-device visibility or revocation.

Patching the artifact route alone relocates the problem. This RFC replaces the model.

Design: three layers, one per job

Layer 1 — Identity: device sessions with pairing

Every browser is a paired device with a server-side record { id, name, created, lastSeen, revoked }.

  • Bootstrap: server prints a one-time login URL (short-lived code) to the terminal — the Jupyter pattern. No static env token as the front door.
  • Additional devices: an authenticated device mints a single-use, short-TTL pairing code/QR (replaces the current permanent-token QR: buzzing someone in once vs copying the house key).
  • Revocation: per device, from a Devices settings panel (list, rename, last-seen, revoke).

Layer 2 — Transport: HttpOnly cookie, opaque and rotating

Pairing sets an HttpOnly cookie holding an opaque random session id (hash stored server-side — nothing signed to steal; lookup = instant revocation), SameSite=Lax, Secure with localhost exemption, sliding expiry with rotation.

Consumer Today Under cookie auth
fetch() API calls Bearer header cookie (header dies)
WebSocket upgrade ?token= in URL cookie + Origin check
<img> in chat, <video>, downloads exempt route authenticated ✓
Artifact preview iframe document exempt route authenticated ✓ (ancestor chain is the same-site app, so Lax attaches)
Sub-resources inside sandboxed artifact HTML exempt route ✗ irreducible gap — below
Automation / curl / CI shared static token Layer 3

CSRF arrives with cookies and is handled at the same moment: SameSite=Lax + require the existing x-pi-web-client-id header on all mutating routes + Origin validation on WS. Cross-origin pages cannot add custom headers without a CORS preflight that is never granted.

Layer 3 — Programmatic access: managed API tokens

Named, mintable, revocable API tokens (hashed at rest, settings UI), Bearer-in-header only, never in URLs. PI_WEB_TOKEN survives one release as a compat alias, then at most seeds the first pairing.

The irreducible gap, decided deliberately

A sandboxed iframe without allow-same-origin has an opaque origin, which nulls the site-for-cookies chain: Lax/Strict cookies do not attach to its sub-resource fetches. SameSite=None collides with third-party cookie blocking; allow-same-origin is a non-starter (model-authored HTML would gain same-origin access to the app); service workers cannot intercept opaque-origin clients. Every design meets this fork, so choose consciously:

Migration (no flag-day)

  1. Add: device sessions + cookie issuance; all routes accept cookie or legacy token; Devices settings panel; pairing QR.
  2. Switch defaults: frontend drops Bearer/localStorage and WS ?token=; token-share QR becomes pairing QR; PI_WEB_TOKEN only bootstraps a first pairing.
  3. Close the hole: artifact route requires the cookie; /api/web-assets/ ships as the narrow public surface; legacy token acceptance removed. The e2e token.spec.ts suite becomes the pairing/cookie suite.

Residual risks (stated, not hidden)

  • Extension-rendered HTML in the main DOM stays trusted-by-design (extension trust model, not an auth gap — and why HttpOnly matters).
  • A hostile process on the same machine was never in this tool's threat model and still is not.
  • Browser matrix for the opaque-origin cookie behavior should be verified explicitly (Chrome/Safari/Firefox) during phase 3.

Open questions

  1. Cookie lifetime/rotation cadence (sliding 30d + rotate weekly?).
  2. Whether the one-time bootstrap URL should auto-trust localhost on first run or always require the printed code.
  3. Interaction with Multi-user architecture: hub router, runtime policy, and containerized solo mode (keep core single-user) #45 (multi-user hub): device sessions are per-user identity primitives there — layer boundaries should be compatible.
  4. Whether /api/session-artifacts/ needs per-session scoping rules beyond the cookie (viewer-scoped artifacts).

Related: #82 (assets milestone auth line item — resolved as a corollary), #27 (vendored libs = same public-assets surface), #45 (multi-user identity).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions