Authentication Challenge
WebSocket connections start with an HTTP Upgrade handshake — this is the only HTTP request in the connection lifecycle. Once upgraded, the connection is a raw TCP stream with no HTTP headers.
This creates an authentication challenge: standard HTTP authentication mechanisms (Bearer tokens in the Authorization header) cannot be sent in WebSocket frames, because Authorization is an HTTP header that only exists during the handshake.
The WebSocket spec does not mandate an authentication approach, leaving several patterns in common use — each with different security trade-offs.
Token in Query String
Pass the token as a URL query parameter:
const token = await getAccessToken();
const ws = new WebSocket(`wss://api.example.com/ws?token=${token}`);
Server-side verification:
async def websocket_handler(websocket, path):
params = urllib.parse.parse_qs(urllib.parse.urlparse(path).query)
token = params.get('token', [None])[0]
user = verify_token(token)
if not user:
await websocket.close(1008, 'Unauthorized')
return
...
Security risk: query parameters appear in server access logs, browser history, HTTP Referer headers, and proxy logs. Only use this pattern if tokens are short-lived (< 5 minutes) and single-use.
Token in First Message
Complete the WebSocket handshake without authentication, then require the client to send an auth token as the first message. Close the connection if the token is not received within a deadline:
import asyncio
async def websocket_handler(websocket):
# Wait for auth message, timeout in 5 seconds
try:
auth_msg = await asyncio.wait_for(websocket.recv(), timeout=5)
data = json.loads(auth_msg)
if data.get('type') != 'auth':
await websocket.close(1008, 'Expected auth message')
return
user = verify_token(data['token'])
if not user:
await websocket.close(1008, 'Invalid token')
return
except asyncio.TimeoutError:
await websocket.close(1008, 'Auth timeout')
return
# Connection is now authenticated — proceed
await handle_authenticated(websocket, user)
Pros: token stays out of URLs and logs. Clean separation of handshake and auth. Cons: there is a brief window where an unauthenticated connection exists.
Cookie-Based Auth
Browsers automatically send cookies in the WebSocket handshake request:
// No extra code needed — session cookie is sent automatically
const ws = new WebSocket('wss://api.example.com/ws');
async def websocket_handler(websocket):
# Access cookies from the initial HTTP handshake
cookies = websocket.request_headers.get('Cookie', '')
session_id = parse_session_cookie(cookies)
user = session_store.get(session_id)
...
Pros: seamless for browser clients already using session cookies. No token management in JavaScript.
CSRF protection: since cookies are sent automatically, a malicious site could initiate a WebSocket connection to your API using the victim's cookies. Mitigate by checking the Origin header against a whitelist.
Ticket-Based Auth
Generate a short-lived, single-use ticket via an authenticated REST endpoint, then use the ticket in the WebSocket URL:
# 1. Client requests a short-lived ticket
# POST /api/ws-ticket (authenticated with Bearer token)
# Response: { "ticket": "abc123", "expires_in": 30 }
# 2. Client connects with the ticket
# ws = new WebSocket('wss://api.example.com/ws?ticket=abc123')
# 3. Server validates and invalidates the ticket
async def websocket_handler(websocket):
ticket = get_query_param(websocket, 'ticket')
user = ticket_store.consume(ticket) # single-use, 30s expiry
if not user:
await websocket.close(1008, 'Invalid or expired ticket')
return
...
This is the recommended pattern — the ticket in the URL is safe because it expires in seconds and can only be used once.
Re-Authentication on Reconnect
Long-lived WebSocket connections must handle token expiry. Strategies:
- Token refresh via first message: on reconnect, always send a fresh token as the first message
- Server-initiated close on expiry: server closes with code 1008 when the session expires; client re-authenticates and reconnects
- Refresh message: define a message type (
{"type": "refresh", "token": "..."}) that the server accepts to extend the session mid-connection
Security Considerations
- Always verify the
Originheader during the handshake to prevent cross-site WebSocket hijacking - Use
wss://(WebSocket over TLS) in production — neverws:// - Set a short idle timeout: close unauthenticated connections after 5–10 seconds if auth is not received
- Rate-limit WebSocket connection attempts per IP to prevent credential stuffing via query-string token brute-force