Dictation WebSocket Authentication vs HTTP API in VoiceStudio: How WS-Ticket Exchanges Work
VoiceStudio uses session-cookie authentication for HTTP endpoints but requires a short-lived, single-use ws-ticket for WebSocket dictation streams to improve security and reduce overhead.
The debpalash/VoiceStudio backend implements a dual authentication system that separates long-lived session management from real-time streaming security. Standard REST endpoints rely on persistent session cookies, while the live dictation WebSocket uses an ephemeral token exchange pattern. This design prevents sensitive session credentials from being exposed on persistent socket connections while maintaining a lightweight handshake for audio streaming.
HTTP API Authentication: Session-Cookie Based
The VoiceStudio HTTP API follows a traditional session-based authentication flow common in FastAPI applications.
How HTTP Authentication Works
- Login endpoint (
POST /api/auth/login) validates user credentials and returns a signed session cookie - Subsequent requests include this cookie in the
Cookieheader - Server validates the cookie signature on every request before processing
This pattern works well for stateless request-response cycles where each call is independent. The session cookie persists across the user's browsing session, allowing convenient access to protected endpoints like user settings, transcription history, and file management.
Key HTTP Authentication Files
backend/api/routers/auth.py— Contains login/logout endpoints and session middleware configuration- Session cookies are configured with standard HTTPOnly and Secure flags in production
WebSocket Authentication: The WS-Ticket Pattern
Real-time dictation introduces unique security challenges. Maintaining a long-lived session cookie on a persistent WebSocket connection increases the attack surface if the connection is compromised. VoiceStudio solves this with a ws-ticket exchange mechanism.
Step 1: Request a WS-Ticket via HTTP
The client first calls a dedicated HTTP endpoint to exchange their valid session for a temporary ticket.
import requests
# Existing session from prior HTTP authentication
session_cookie = {"session": "eyJ1c2VyX2lkIjogMTIzLCAiZXhwIjogMTY5ODU0NDYzMn0..."}
# Exchange session for ws-ticket
response = requests.post(
"https://api.voicestudio.example/api/auth/ws-ticket",
cookies=session_cookie
)
# Extract the single-use ticket
ws_ticket = response.json()["ws_ticket"]
# Example: "ws_tkt_a3f8b2d9e7c1f5a4b8e2d9c7f1a5b3e9"
The POST /api/auth/ws-ticket endpoint in backend/api/routers/auth.py performs these operations:
- Validates the incoming session cookie
- Generates a cryptographically random ticket string
- Stores the ticket in an in-memory cache with user ID mapping
- Sets an expiration timer (typically 30-60 seconds)
- Returns the ticket to the client
Step 2: Establish WebSocket with Ticket
The client includes the ws-ticket when opening the WebSocket connection. VoiceStudio supports this via query parameter:
import asyncio
import websockets
async def connect_dictation_stream():
ws_url = (
f"wss://api.voicestudio.example/ws/dictation"
f"?ws-ticket={ws_ticket}"
)
async with websockets.connect(ws_url) as websocket:
# Ticket is validated during handshake
# Connection upgrades to authenticated dictation channel
# Stream audio chunks
while audio_chunk := microphone.read():
await websocket.send(audio_chunk)
# Receive transcription results
result = await websocket.recv()
print(f"Transcription: {result}")
asyncio.run(connect_dictation_stream())
Step 3: Server-Side Ticket Validation
The WebSocket handler in backend/api/routers/speech_platform.py (or equivalent dictation router) executes:
- Extracts
ws-ticketfrom query parameters - Validates against the in-memory ticket store
- Checks expiration timestamp
- Verifies single-use status (immediately marks as consumed)
- Resolves user identity from ticket mapping
- Upgrades connection or rejects with
401 Unauthorized
# Conceptual implementation based on VoiceStudio pattern
async def ws_dictation_endpoint(websocket: WebSocket, ws_ticket: str):
# Ticket validation
ticket_data = await ticket_store.consume(ws_ticket)
if not ticket_data or ticket_data["exp"] < time.time():
await websocket.close(code=1008, reason="Invalid or expired ticket")
return
user_id = ticket_data["user_id"]
# Upgrade to authenticated session
await websocket.accept()
# Begin dictation streaming...
Key Differences: HTTP vs WebSocket Authentication
| Aspect | HTTP API | WebSocket Dictation |
|---|---|---|
| Credential type | Persistent session cookie | Ephemeral ws-ticket |
| Lifetime | Hours to days | 30-60 seconds |
| Usage count | Unlimited per session | Single-use only |
| Transmission | Cookie header on every request | Query parameter once at handshake |
| Validation location | Every HTTP request middleware | WebSocket accept handler |
| Revocation | Session logout invalidates cookie | Automatic expiration or single-use consumption |
Security Benefits of the WS-Ticket Pattern
The VoiceStudio implementation provides several security advantages:
- No long-lived credentials on socket — If the WebSocket connection is intercepted, the attacker gains no persistent session access
- Replay attack prevention — Single-use consumption means stolen tickets cannot be reused
- Time-bound exposure — Short expiration windows limit the attack window
- Isolated privilege scope — The ticket grants only dictation access, not full API capabilities
Handling Ticket Exchange Failures
Production implementations should handle these edge cases:
import websockets.exceptions
async def robust_dictation_connect():
try:
ws = await websockets.connect(ws_url)
# Check for immediate close (invalid ticket)
try:
await asyncio.wait_for(ws.ping(), timeout=5.0)
except asyncio.TimeoutError:
# Server rejected ticket, request new one
ws_ticket = await refresh_ws_ticket()
# Retry with fresh ticket...
except websockets.exceptions.InvalidStatusCode as e:
if e.status_code == 401:
# Ticket expired or invalid
raise AuthenticationError("WS ticket rejected")
Summary
- HTTP authentication in VoiceStudio uses traditional session cookies managed through
backend/api/routers/auth.py - WebSocket authentication requires a ws-ticket exchange: obtain via
POST /api/auth/ws-ticket, consume once at WebSocket handshake - Tickets are single-use and short-lived (30-60 seconds), stored in-memory and immediately consumed upon validation
backend/api/routers/speech_platform.pyhandles the WebSocket path and ticket verification logic- This dual-mode design separates concerns: persistent sessions for API calls, ephemeral tokens for real-time streaming
Frequently Asked Questions
How long does a ws-ticket remain valid?
VoiceStudio ws-tickets typically expire within 30 to 60 seconds of issuance. This short window balances security with network latency—enough time for the client to complete the WebSocket handshake, but insufficient for meaningful attacker exploitation if intercepted.
Can I reuse a ws-ticket if my WebSocket connection drops?
No. Tickets are single-use by design. If your connection fails, you must call POST /api/auth/ws-ticket again to obtain a fresh ticket. The server marks each ticket as consumed immediately upon successful WebSocket acceptance, preventing replay attacks.
Does the WebSocket connection require the session cookie after ticket validation?
No. Once the ws-ticket is validated during the WebSocket handshake, the server associates the connection with the authenticated user internally. The session cookie is not required for subsequent messages on that socket, which is precisely why the ticket pattern improves security for long-lived connections.
What happens if I send an invalid or expired ws-ticket?
The server responds with a WebSocket close frame using code 1008 (policy violation) and reason "Invalid or expired ticket". The HTTP upgrade fails, and the client receives no further communication on that connection. You should implement retry logic that requests a fresh ticket and reattempts the connection.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →