Skip to content

feat(auth): add os_session cookie + dual-auth on /api/calls/:id/audio#25

Merged
revtex merged 1 commit intodevfrom
feat/session-cookie-audio-auth
Apr 25, 2026
Merged

feat(auth): add os_session cookie + dual-auth on /api/calls/:id/audio#25
revtex merged 1 commit intodevfrom
feat/session-cookie-audio-auth

Conversation

@revtex
Copy link
Copy Markdown
Owner

@revtex revtex commented Apr 25, 2026

Summary

Adds an httpOnly Secure SameSite=Strict session cookie (os_session) so that <audio src=…> requests can authenticate without bearer-header injection, while keeping today's bearer flow working unchanged on every route. Backwards compatible — no client changes in this PR.

Why

Browsers cannot attach Authorization: Bearer to <audio src=…> without going through fetch() + Blob + URL.createObjectURL — exactly the path that fails on Mobile Edge for AAC (Web Audio's decodeAudioData ships codec gaps in Edge's Chromium fork). Switching the audio path to a real URL is the architecturally correct fix; this PR adds the auth piece so a follow-up frontend PR can simply set <audio src="/api/calls/:id/audio"> and have it work.

Auth flow on /api/calls/:id/audio

Authorization header os_session cookie Sec-Fetch-Site publicAccess Result
valid bearer 200 (existing)
valid same-origin / same-site / none / absent 200
valid cross-site false 401
valid cross-site true 200 (anonymous)
invalid/expired false 401
invalid/expired true 200 (anonymous)
false 401
true 200 (anonymous)

Stale credentials never 401 directly — they fall through to anonymous so publicAccess=true semantics keep working. CSRF is mitigated by the SameSite=Strict cookie attribute plus a server-side Sec-Fetch-Site check.

Files

File Change
backend/internal/auth/cookie.go + SetSessionCookie, ClearSessionCookie, SessionCookieName, SessionCookiePath
backend/internal/auth/cookie_test.go + flag-matrix tests
backend/internal/handler/auth/auth.go login + refresh issue cookie; logout clears it
backend/internal/middleware/auth.go + OptionalJWTOrSessionAuth
backend/internal/handler/routes/routes.go wire new middleware on /calls/:id/audio; fix typed-nil interface footgun on hub
backend/internal/handler/routes/auth_test.go + login/refresh/logout cookie tests
backend/internal/handler/routes/audio_test.go (new) + full dual-auth matrix
CHANGELOG.md bullet under Unreleased ### Added

Verification

  • go vet ./... — clean
  • go build ./... — clean
  • go test ./... — all pass (including the new dual-auth matrix)
  • git diff --stat dev..HEAD backend/ only; no frontend touched

Self-review against OWASP

  • A01 Broken access control: the cookie carries the same JWT used by the bearer header; same auth.ParseToken and auth.Tokens.IsRevoked apply. Per-call grants are still enforced inside GetCallAudio. No privilege escalation surface added.
  • A07 Auth failures: stale/invalid credentials fall through to anonymous (matching today's OptionalJWTAuth behaviour) — this preserves publicAccess=true semantics and never leaks a 401 that would mask a still-valid session. Cookie rotates on every refresh; cleared on logout.
  • CSRF: SameSite=Strict + server-side Sec-Fetch-Site check. The route is read-only (GET); the worst case from a successful CSRF on this route is 'attacker causes the victim's browser to GET audio the victim was already authorised for' — no state change, no exfiltration channel beyond what the victim already has.

What's next

Phase 2 (backend: drop audio bytes from the WS CAL payload) and Phase 3 (frontend: switch playback to <audio src=…>) ship as a paired follow-up, merged sequentially. This PR is independently mergeable today.

- New auth.SetSessionCookie / ClearSessionCookie helpers (HttpOnly,
  Secure when HTTPS, SameSite=Strict, Path=/api)
- POST /api/auth/login and /api/auth/refresh issue os_session alongside
  the existing access JWT response; POST /api/auth/logout clears it
- New middleware.OptionalJWTOrSessionAuth resolves identity from, in
  priority order: bearer header, os_session cookie (guarded by
  Sec-Fetch-Site), anonymous
- GET /api/calls/:id/audio swapped to the new middleware; every other
  route is unchanged. Bearer flow continues to work everywhere.
- Routes.RegisterRoutes now promotes deps.Hub into the WSDisconnecter
  interface only when the concrete pointer is non-nil, fixing a
  pre-existing typed-nil interface footgun on the logout path.

Tests:
- backend/internal/auth/cookie_test.go: SetSessionCookie /
  ClearSessionCookie flag matrix
- backend/internal/handler/routes/auth_test.go: login/refresh/logout
  cookie issuance and rotation
- backend/internal/handler/routes/audio_test.go (new): full dual-auth
  matrix on the audio route — bearer, cookie+same-origin, cookie+missing
  Sec-Fetch-Site, cookie+cross-site (publicAccess on/off), stale cookie
  fallthrough (publicAccess on/off), anonymous (publicAccess on/off)

go vet, go build, go test ./... all clean.
Backwards compatible — no frontend changes.
@revtex revtex merged commit 6f49a8a into dev Apr 25, 2026
7 checks passed
@revtex revtex deleted the feat/session-cookie-audio-auth branch April 25, 2026 19:31
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