Skip to content

fix(security): short-lived signed tickets for stream + WebSocket auth (#30)#79

Merged
windoze95 merged 1 commit into
mainfrom
feature/backend-stream-tickets
Jun 28, 2026
Merged

fix(security): short-lived signed tickets for stream + WebSocket auth (#30)#79
windoze95 merged 1 commit into
mainfrom
feature/backend-stream-tickets

Conversation

@windoze95

Copy link
Copy Markdown
Owner

Move the long-lived session token out of /stream, /preview-stream, and the WebSocket handshake URL query strings (where it leaks to proxy/access logs, history, and shared links) into short-lived HMAC-signed tickets (roadmap LATER tier, #10 / issue #30). Additive / backward-compatible — the old ?token= keeps working during transition; clients switch in a follow-up. No migration.

Tickets (app/utils/tickets.py)

  • Wire format base64url(payload).base64url(hmac_sha256(payload)); compact payload {scope, user_id, video_id?, exp}. Signature verified first, constant-time (hmac.compare_digest) before any field is trusted; rejects tampered/expired/wrong-scope/wrong-user/wrong-video (with a bool-vs-int exp guard). TTL 5 min. Scopes stream (video+user) and ws (user) — a ws ticket can't be replayed on /stream.
  • Signing secret: stable + shared — STREAM_TICKET_SECRET env if set, else a secret generated once and persisted under config_path via an atomic hard-link claim (concurrency-safe across workers, 0600). Never per-process-random, so worker A's ticket verifies on worker B.

Endpoints

  • Mint (session-auth): POST /api/videos/{id}/playback-ticket → stream ticket; POST /api/auth/ws-ticket → ws ticket. Both return {ticket, expires_in}.
  • /stream + /preview-stream: check ?ticket= (verify scope+video) first, else fall back to ?token=/X-User-Token.
  • WS: check ?ticket= (verify scope+user) first, else fall back to ?token=.

Verification (local)

  • pytest -q224 passed (+ test_stream_tickets.py: mint→stream works, expired/tampered/wrong-video/wrong-user rejected, WS accepts a valid ws-ticket and still 4401s a bad one, and ?token= still works on stream + WS).
  • ruff clean · no migration.

Follow-up: switch the Flutter + tvOS stream/WS URL builders to fetch a ticket. The old query token still works until then.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RXMKM1rDWn8wNh93MMUtxY

#30)

The long-lived session token used to ride /stream, /preview-stream and the
WebSocket handshake as a ?token= query param, so it leaked into proxy/access
logs, browser history, and shared links — one log line was a full session
hijack (issue #30 / review #10).

Add HMAC-signed, URL-safe access tickets minted from a session-authenticated
request and scoped narrowly:
- app/utils/tickets.py: mint/verify helper. A ticket is
  base64url(payload).base64url(hmac_sha256(payload)) over compact JSON
  {scope, user_id, video_id?, exp}, 5-minute TTL, constant-time verification
  (hmac.compare_digest). Rejects tampered/expired/wrong-scope/wrong-user/
  wrong-video.
- Stable signing secret: read from settings.stream_ticket_secret
  (env STREAM_TICKET_SECRET); when unset, generate once and persist under
  config_path via an atomic hard-link claim so it is stable across restarts
  and shared by all workers (never a per-process random value). Documented in
  .env.example.

Mint endpoints (session-authenticated):
- POST /api/videos/{id}/playback-ticket -> {ticket, expires_in} (stream scope,
  bound to the user + that video)
- POST /api/auth/ws-ticket -> {ticket, expires_in} (ws scope, bound to the user)

Acceptance is additive and backward-compatible: /stream, /preview-stream and
/ws/{user_id} check a ticket first, then fall back to the existing ?token=
session param (or X-User-Token header for streams). No client break; clients
switch to tickets in a follow-up.

Tests: tickets crypto (roundtrip, wrong scope/user/video, expired, tampered,
malformed, secret rotation), mint endpoints (auth required, unknown video 404),
stream/preview-stream over ?ticket= (incl. ranges) with expired/wrong-video/
tampered rejected and ?token= still working, and WS accepting a valid ws-ticket,
4401ing a bad one, and still honoring ?token=. Full suite: 224 passed; ruff
check + format clean. No schema change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMKM1rDWn8wNh93MMUtxY
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@windoze95 windoze95 merged commit 581283a into main Jun 28, 2026
5 checks passed
@windoze95 windoze95 deleted the feature/backend-stream-tickets branch June 28, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant