Auth

Summary

Telemy’s authentication system is built on three distinct security layers: an account/entitlement layer (OAuth + email/password login gating paid features), a control-plane API layer (short-lived cp_access_jwt protecting backend calls from the OBS plugin), and a relay publish/play layer (per-user stream_token controlling SLS stream access). The OBS plugin authenticates via a browser-based login attempt flow: the plugin calls POST /auth/plugin/login/start, opens the system browser to a login page, and polls POST /auth/plugin/login/poll until the user completes sign-in. Credentials (cp_access_jwt + refresh_token) are stored in DPAPI vault on Windows. The dock JS never receives control-plane secrets.

User-facing authentication supports Twitch, Google, and Discord OAuth plus email/password signup. OAuth tokens are encrypted at rest with AES-256-GCM. Account linking uses verified-email merge: when a new OAuth sign-in matches an existing account’s email, a time-limited merge request requires re-authentication with the original provider before linking. The login page at telemyapp.com/login replaced an earlier operator-only approval page and supports the plugin login attempt flow via ?attempt=<id> query parameter.

A critical security refactor (C2) moved the web dashboard JWT from localStorage to an HttpOnly telemy_session cookie. Cloudflare Pages Functions act as a BFF proxy, reading the cookie and forwarding requests to the Go backend with a Bearer header. The OBS plugin is unaffected — it continues sending Bearer tokens directly to api.telemyapp.com. CSRF is mitigated by SameSite=Strict on the cookie.

Timeline

  • 2026-03-16: Initial auth login + entitlement plan drafted. Defined the three-layer security model, plugin login attempt flow, token model (cp_access_jwt 10-15min TTL, refresh_token 30-day TTL), and five implementation slices (A through E). Recommended starting with Slice A (server-side relay entitlement enforcement).
  • 2026-03-16: RF-001 per-user stream ID auth design approved. Replaced hardcoded live_telemy/play_telemy with per-user stream_token (8-char alphanumeric, ~2.8 trillion combinations). Approach A (SLS-layer stream ID) selected over IP allowlist (breaks mobile) and HMAC auth (breaks all existing clients).
  • 2026-03-16: RF-001 implementation plan created with 9 tasks across Go backend, C++ plugin, and dock UI. Database migration 0007_add_stream_token, store/model/API changes, user-data template parameterization, and dock connection details update.
  • 2026-03-22: Pricing tiers finalized — Free (9.99/mo, 1 managed relay included, up to 3 add-ons at $7.99/mo each). Hidden 48 Mbps soft cap per relay.
  • 2026-03-24: W2 OAuth auth redesign approved. Added Twitch, Google, Discord OAuth + email/password with account linking and merge confirmation. Three implementation phases: A (database + OAuth infrastructure), B (OAuth endpoints), C (email/password + login page). New tables: user_oauth_accounts, oauth_states, merge_requests. Migration 0020.
  • 2026-03-25: C2 HttpOnly cookie design approved (CRITICAL security). JWT moved from localStorage to HttpOnly telemy_session cookie proxied through CF Pages Functions. 11-task implementation plan covering cookie helpers, API proxy, OAuth/email login flows, dashboard auth gate, and localStorage removal.

Current State

The auth system uses three layers: OAuth/email account login, cp_access_jwt for control-plane API calls, and per-user stream_token for relay stream access. The plugin login flow works via browser-based login attempts with poll-based completion. Refresh tokens rotate on each use; JWT + refresh are stored in DPAPI vault on the plugin side.

OAuth providers (Twitch, Google, Discord) and email/password are the supported sign-in methods. OAuth tokens are encrypted at rest with AES-256-GCM keyed by OAUTH_ENCRYPTION_KEY. Account linking uses email-match merge requests with 10-minute expiry.

The web dashboard uses HttpOnly telemy_session cookie for authentication. CF Pages Functions proxy all /api/v1/* requests to the Go backend, attaching the JWT from the cookie as a Bearer header. The Go backend itself is unchanged — it validates Authorization: Bearer regardless of whether the caller is the web proxy or the OBS plugin.

Entitlement enforcement is server-side: POST /api/v1/relay/start checks relay_access_status and ActiveManagedConns < MaxConcurrentConns before provisioning. Users can still inspect/stop relays after plan changes (ownership-based, not entitlement-based).

Billing integration (LemonSqueezy) is independent of the auth method — webhooks update plan_tier by user ID. Grace period is 48 hours after payment failure before downgrade; force-disconnect terminates active relay sessions on downgrade.

Key Decisions

  • 2026-03-16: Browser-based login attempt flow over embedded CEF login — CEF is a bad trust boundary for control-plane secrets; browser login supports subscription checkout cleanly; no loopback callback server required.
  • 2026-03-16: Per-user stream ID at SLS layer (Approach A) over IP allowlist or HMAC auth — zero cost, zero latency, works with all existing SRTLA clients (IRL Pro, Larix, etc.).
  • 2026-03-16: License keys rejected as primary UX — easy to copy/share, awkward revocation, worse UX than browser login. Acceptable only as internal fallback.
  • 2026-03-16: Custom SRTLA REG1/REG2 auth rejected as primary product gate — breaks third-party sender app compatibility. Transport hardening only, not the main access control mechanism.
  • 2026-03-16: cp_access_jwt short-lived (10-15 min TTL) with rotating refresh_token (30-day TTL) — JWT proves identity/session but does not grant relay access without separate entitlement check.
  • 2026-03-22: Pricing: Free tier gets full telemetry + unlimited BYOR but zero managed relays. Standard at 7.99/mo each (max 3 add-ons, 4 total). Hidden 48 Mbps soft cap.
  • 2026-03-24: OAuth providers: Twitch (chat scopes), Google/YouTube (live chat scopes), Discord (identify + email + guilds). Email/password as fourth option for non-streamer users.
  • 2026-03-24: OAuth token encryption: AES-256-GCM at rest, key from OAUTH_ENCRYPTION_KEY env var. Tokens never exposed to the OBS plugin — plugin only sees linked: true/false.
  • 2026-03-24: Account merge flow: email collision triggers a 10-minute merge request requiring re-auth with the original provider. Prevents account hijacking via shared email.
  • 2026-03-25: JWT moved from localStorage to HttpOnly cookie (C2 CRITICAL) — XSS can no longer exfiltrate tokens remotely. CF Pages Functions BFF proxy pattern chosen to keep Go backend unchanged.
  • 2026-03-25: Cookie spec: telemy_session, HttpOnly, Secure, SameSite=Strict, Path=/, domain telemyapp.com only (not .telemyapp.com). Cookie never sent directly to api.telemyapp.com subdomain.

Experiments & Results

ExperimentStatusFindingSource
RF-001 Approach A: Per-user stream ID at SLS layerSelectedZero cost, zero latency, works with all SRTLA clients. 8-char token = ~2.8T combinations.rf001-per-user-stream-id-auth-design.md
RF-001 Approach B: IP allowlist at srtla_recRejectedBreaks mobile streaming — cellular IPs change constantly.rf001-per-user-stream-id-auth-design.md
RF-001 Approach C: HMAC auth in SRTLA protocolRejectedBreaks all existing clients (IRL Pro, Larix, etc.), XL effort.rf001-per-user-stream-id-auth-design.md
localStorage JWT (pre-C2)ReplacedXSS could exfiltrate token and use from any machine for full lifetime.c2-httponly-cookie-design.md
HttpOnly cookie + BFF proxy (C2)ImplementedBrowser JS cannot read JWT. CSRF blocked by SameSite=Strict. XSS in-session API calls still possible (inherent).c2-httponly-cookie-design.md

Gotchas & Known Issues

  • cp_access_jwt must never be exposed to dock JS. The dock receives only a safe auth state summary (signed in/out, entitled/unentitled). The bridge must not forward JWT or refresh tokens.
  • stream_token is permanent per user. Token regeneration endpoint (POST /api/v1/user/regenerate-tokens) is designed but deferred. If a streamer leaks their stream ID on camera, there is currently no self-service way to rotate it.
  • Operator approval page is transitional. The browser step at telemyapp.com/login/plugin?attempt=... was initially operator-assisted. The W2 OAuth redesign replaces it with self-service login at telemyapp.com/login?attempt=....
  • JWT signing uses raw string, not hex-decoded bytes. See feedback_jwt_signing.md in memory files for details on this implementation choice.
  • Email sending deferred for v1. Email verification URLs are logged to server logs rather than sent via an email service. Password reset and verification flows require manual operator intervention until an email provider is integrated.
  • bcrypt cost 12 for passwords. Rate limiting: 5 attempts/min per IP on login, signup, and forgot-password endpoints.
  • Discord bot integration deferred. Discord OAuth works for sign-in and account linking, but the bot-to-server flow (24/7 WebSocket gateway) is a separate phase.
  • XSS in-session risk remains after C2. HttpOnly cookies prevent token exfiltration but an XSS attack can still make API calls within the user’s session. Future CSP headers (L-items in security audit) address this.
  • TELEMY_OPERATOR_PASSWORD kept for admin access even after OAuth rollout. Operator password secures admin endpoints separately from user auth.
  • Plugin login attempt expiry is 5 minutes with a one-time poll token. If the user doesn’t complete browser auth in time, the attempt expires (410 response).

Open Questions

  • Will JWT uid equal internal users.id, or is external identity mapping needed? (From initial plan, 2026-03-16)
  • Is quota exhaustion enforced in v1 or only reported? (Entitlement plan open decision)
  • Should license-key activation exist as a fallback, or go directly to browser login only? (Decided against as primary UX, but fallback status unclear)
  • When will an actual email service be integrated for verification and password reset? (Currently logged to server only)
  • Timeline for CSP headers to mitigate remaining in-session XSS risk? (Classified as L-item in security audit)
  • Timeline for stream_token regeneration endpoint? (Design supports it, implementation deferred)

Sources

  • AUTH_ENTITLEMENT_MODEL.md
  • 2026-03-16-auth-login-entitlement-plan.md
  • 2026-03-16-rf001-per-user-stream-id-auth-design.md
  • 2026-03-16-rf001-per-user-stream-id-auth.md
  • 2026-03-24-w2-oauth-auth-plan.md
  • 2026-03-24-w2-oauth-auth-redesign.md
  • 2026-03-25-c2-httponly-cookie-design.md
  • 2026-03-25-c2-httponly-cookie-plan.md