Authentication & Security for Real-Time Connections

WebSockets & Real-Time Communication — Authentication & Security for Real-Time Connections
WebSockets & Real-Time Communication
Chapter 8 · Authentication & Security for Real-Time Connections

🔐 Authentication & Security for Real-Time Connections

Every chapter so far quietly assumed a well-behaved client. This chapter drops that assumption — the same lessons the XSS, SQL Injection, and Authentication & Session Security courses taught about HTTP apply here too, just delivered over a connection that stays open far longer than any single request.

Authenticating Once, at the Handshake

A typical HTTP API checks auth on every request. A WebSocket authenticates once, at connect time — whatever identity is established during the handshake applies for the entire life of that connection, so getting the handshake right matters more here than it would for any single HTTP endpoint.

Where to Put the TokenTrade-off
Query string (?token=...)Simple, but tokens can leak into server/proxy access logs since they're part of the URL
CookieSent automatically on same-origin handshakes (it's still an HTTP request) — reuses the session cookie work from the Authentication course
First message after connectAvoids URL/cookie exposure, but means briefly accepting an unauthenticated connection before validating it

Rejecting Before the Upgrade Completes

const wss = new WebSocketServer({ port: 8080, verifyClient(info, callback) { const token = new URL(info.req.url, "http://x").searchParams.get("token"); if (!isValidToken(token)) { return callback(false, 401, "Unauthorized"); // rejected before the 101 response } callback(true); } });

Rejecting inside verifyClient stops a bad handshake before the Chapter 2 Upgrade completes — cheaper than accepting the connection and immediately closing it.

🌍 Origin Checking

The Origin header is present during the handshake, since it's still an HTTP request — checking it against an allowlist stops a malicious page on a different origin from opening a WebSocket to your server using a logged-in user's browser session. This is the exact same threat the CSRF course covers, just aimed at a WebSocket handshake instead of a form submission.

verifyClient(info, callback) { const allowed = ["https://example.com"]; if (!allowed.includes(info.origin)) { return callback(false, 403, "Forbidden origin"); } callback(true); }

⏱️ Rate Limiting Per Connection

HTTP rate limiting counts requests. A single WebSocket connection can send unlimited messages over the same open socket — limiting needs to happen per connection, not per request:

A Minimal Token Bucket Per Socket

function attachRateLimit(socket, maxPerSecond = 10) { let tokens = maxPerSecond; setInterval(() => { tokens = maxPerSecond; }, 1000); const original = socket.emit.bind(socket); socket.on("message", () => { if (tokens <= 0) { socket.close(1008, "Rate limit exceeded"); // policy violation close code return; } tokens--; }); }

🧪 Validating Every Incoming Message

The same "never trust input" lesson from the SQL Injection and XSS courses applies directly here — JSON.parse'd data is not automatically safe just because it parsed successfully. Validate structure and types before using it:

socket.on("message", (raw) => { let data; try { data = JSON.parse(raw.toString()); } catch { return; // not even valid JSON — drop it } if (typeof data.text !== "string" || data.text.length > 500) { return; // wrong shape — never assume the client sent what the UI intended } broadcastToRoom(data.roomName, data.text); });

Trusting the Client vs Not

Naive

Parse, then immediately broadcast whatever came through — no shape check, no auth check beyond the initial handshake, no rate limit.

This Chapter

Authenticated at handshake, origin-checked, rate-limited per connection, and every message validated before it's ever broadcast to anyone else.

💻 Coding Challenges

Challenge 1: Add Origin Checking to verifyClient

Combine the token-check and origin-check examples from this chapter into a single verifyClient function that rejects a connection failing either check, with the correct distinct status code for each failure.

Goal: Practice composing multiple handshake-time checks correctly.

→ Solution

Challenge 2: Validate a Chat Message Shape

Write a validateChatMessage(data) function that returns true only if data has a roomName (non-empty string), a text (non-empty string, max 500 characters), and no other unexpected fields.

Goal: Practice writing a strict shape check rather than trusting a partially-correct message.

→ Solution

Challenge 3: Identify a Stored XSS Vector

A chat client renders every incoming message with messageDiv.innerHTML = data.text. Explain exactly how this becomes a stored XSS vulnerability, referencing the XSS course, and provide the fix.

Goal: Practice recognizing that a WebSocket message is exactly as untrusted as any other user input.

→ Solution

⚠️ Gotcha: Rendering a Broadcast Message With innerHTML

It's easy to forget, in the middle of building rooms and presence and reconnection logic, that a chat message is still just untrusted user input — exactly the kind the XSS course spent an entire chapter on. If one client sends <img src=x onerror="steal_cookies()"> as their chat text and every other client's UI renders it with innerHTML, that script now runs in every other user's browser the moment the room broadcasts it — a textbook stored XSS attack, just delivered over a WebSocket instead of a database-backed page load. The fix is the same one that course taught: use textContent instead of innerHTML for plain text, or run untrusted HTML through a sanitizer (like DOMPurify) if formatting genuinely needs to be preserved.

🎯 What's Next

Every piece is now in place — the final chapter is a capstone: Building a Real-Time Feature, a live chat application pulling together rooms, authentication, reconnection, and broadcasting from every chapter in this course.