feat(auth): optional WEBTERM_TOKEN access-token gate (W5)
An OPTIONAL shared token so the app can be used off-LAN (via the relay/tunnel) more safely than "anyone who reaches the port gets a shell". Sits IN FRONT OF the existing Origin/CSRF model — never replacing it — and is fully inert when unset. - src/http/auth.ts (new, pure): constantTimeEqual hashes both inputs to sha256 (32 bytes) then crypto.timingSafeEqual — no `===`, no length side-channel, never throws. parseCookieHeader / cookieIsAuthed / buildSetCookie / isAuthEnabled. - WEBTERM_TOKEN in config: 16–512 URL/cookie-safe chars or the server refuses to start. - GET /?token=<t> or POST /auth (rate-limited 10/min) validates → sets HttpOnly; SameSite=Strict; Secure-when-https cookie; public/login.html (no <script>). - When enabled: the WS handshake (AFTER the Origin check, before handleUpgrade) + a central authGate over all remote HTTP require the cookie. Open: /login, /auth. Loopback bypass scoped to /hook* ONLY (tighter than the plan — gates the local browser too). - Unset ⇒ isAuthEnabled false ⇒ pure passthrough (LAN zero-config unchanged). Honest tradeoff (in code + CLAUDE.md + login page): a bar-raiser, NOT a TLS/Tailscale substitute — on bare ws:// the token is cleartext + replayable; only hardens the TLS-terminated relay path. Verified: typecheck + build:web clean, 2118 pass at --test-timeout=30000 (disabled-mode regression proven; only the known tmux flake red); auth.ts 100% line coverage.
This commit is contained in:
@@ -65,6 +65,8 @@ npm test # unit tests (vitest, all modules)
|
||||
|
||||
Config is via env vars only (no hardcoding): `PORT`, `SHELL_PATH`, `BIND_HOST`, `IDLE_TTL`, `SCROLLBACK_BYTES`, `MAX_PAYLOAD_BYTES`, `USE_TMUX` (1/0/auto), `ALLOWED_ORIGINS`. Note `allowedOrigins` is derived from the host's network-interface IPs (not from `BIND_HOST` — `0.0.0.0` is never a valid Origin); see ARCHITECTURE §3.1.
|
||||
|
||||
`WEBTERM_TOKEN` (w5-access-token, optional) — a shared access token that gates the WS handshake (alongside, not replacing, the Origin check) and every remote HTTP route. **Unset ⇒ auth disabled**, so LAN zero-config is preserved exactly as before; only when set does the gate activate. When set it must be 16–512 URL/cookie-safe chars (`[A-Za-z0-9._~+/=-]`) or the server refuses to start. Deliver it once via `GET /?token=<t>` (or `POST /auth`), which sets an `HttpOnly; SameSite=Strict; Secure-when-https` cookie the browser auto-sends thereafter; loopback hook ingest (`/hook*`) is exempt so the smart-features side-channel keeps working. **Honest tradeoff:** it is a bar-raiser, **not** a TLS/Tailscale substitute — on bare `ws://` the token travels in cleartext and is replayable by a LAN sniffer; it only meaningfully hardens the relay/tunnel (TLS-terminated) path. Never port-forward the raw port to the internet. See `src/http/auth.ts` and `docs/plans/w5-access-token.md`.
|
||||
|
||||
## Architecture (the parts that span files)
|
||||
|
||||
The server is a **byte-shuttle, not a terminal**. It does not parse ANSI/terminal semantics — xterm.js (browser) interprets escape sequences and renders; node-pty (server) provides the pseudo-terminal so the shell believes it has a real TTY. This separation is the central simplification — keep it. Don't add terminal-semantic parsing on the server.
|
||||
|
||||
Reference in New Issue
Block a user