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.

The VoiceStudio HTTP API follows a traditional session-based authentication flow common in FastAPI applications.

How HTTP Authentication Works

  1. Login endpoint (POST /api/auth/login) validates user credentials and returns a signed session cookie
  2. Subsequent requests include this cookie in the Cookie header
  3. 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:

  1. Extracts ws-ticket from query parameters
  2. Validates against the in-memory ticket store
  3. Checks expiration timestamp
  4. Verifies single-use status (immediately marks as consumed)
  5. Resolves user identity from ticket mapping
  6. 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.py handles 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →