# WebSockets — Part 2: Origin Validation, Auth, Reconnection, and Framing

Part 1 covered the handshake and the basic full-duplex API. This part covers the security-relevant gap the handshake leaves open, how real applications authenticate a WebSocket connection, and the client-side resilience patterns (reconnection, message framing) every production WebSocket client needs.

## 1. WebSocket Does NOT Enforce Same-Origin — the Server Must Check Itself

This is the single most important security fact about WebSockets, and it's easy to miss because everything else in this group (fetch, CORS, cookies) trains you to expect the browser to enforce origin restrictions automatically. **It does not, for WebSocket connections.**

`fetch()` to a cross-origin URL is gated by CORS (the CORS topic) — the browser sends the request but withholds the response from JS unless the server opts in. A cross-origin `new WebSocket(url)` handshake, by contrast, is **allowed to complete and open successfully by default**, with no CORS-style preflight or opt-in header required at all. If a malicious page does:

```js
// evil.example's page, opening a WebSocket to a victim's service
const socket = new WebSocket("wss://api.bank.example.com/live-feed");
```

...the browser will happily attempt this handshake, and any cookies for `api.bank.example.com` (subject to `SameSite`, exactly as in the CSRF topic) get attached to the handshake's underlying HTTP request. If the server accepts the connection based on the cookie alone, without checking where the request actually came from, `evil.example` has just opened an authenticated real-time connection to the victim's account — a WebSocket-flavored analog of CSRF.

**The fix is entirely server-side**: the server must inspect the `Origin` header (which the browser *does* still send honestly on the handshake request, even though it won't enforce anything based on it) and reject handshakes from origins it doesn't recognize:

```js
// server-side (e.g. in a Node WebSocket server's upgrade handler)
function isAllowedOrigin(origin) {
  return origin === "https://app.bank.example.com";
}

server.on("upgrade", (request, socket, head) => {
  if (!isAllowedOrigin(request.headers.origin)) {
    socket.destroy(); // reject the handshake outright
    return;
  }
  // proceed with the WebSocket upgrade
});
```

This is the exact same "browser sends it, server must verify it" pattern already established for CSRF — WebSocket just makes it more surprising, because there's no automatic CORS-style gate to lean on at all.

## 2. Authentication: Cookies Aren't the Whole Story

Because the handshake is a real HTTP request, a session cookie CAN authenticate it (subject to the origin-check requirement from §1). But two common alternatives exist, often preferred specifically because they sidestep cookie/CSRF-adjacent concerns:

- **Auth token in the connection URL or a subprotocol**: `new WebSocket("wss://api.example.com/socket?token=eyJ...")`. Simple, but the token can end up in server access logs and browser history — generally discouraged for long-lived secrets, more acceptable for short-lived, single-use connection tokens minted specifically for this purpose.
- **Post-connect authentication message**: connect first, then immediately send an auth message as the first application-level frame, and have the server hold the connection in an "unauthenticated" state (rejecting all other message types) until it validates that first message:
  ```js
  socket.onopen = () => {
    socket.send(JSON.stringify({ type: "auth", token: sessionToken }));
  };
  ```
  This keeps the credential out of the URL/logs entirely, at the cost of a small window where the connection is open-but-unauthenticated (the server must actively enforce that window, not just trust `onopen`).

Whichever mechanism is used, it does **not** replace the origin check from §1 — token-based auth defeats a cross-origin attacker who doesn't have a valid token, but a server should still reject connections from unexpected origins as defense-in-depth, exactly as the CSRF topic recommends layering `SameSite` and tokens together rather than relying on just one.

## 3. Reconnection: the Client's Job, Not the Protocol's

Part 1 noted that `onclose` fires on any disconnection, clean or not, and that the protocol gives no delivery guarantees across a dropped connection. Production clients need explicit reconnection logic — and naive immediate-retry-in-a-loop is a real operational hazard: if a server restarts and every client tries to reconnect instantly and simultaneously, the resulting connection storm can prevent the server from ever finishing its restart. The standard fix is **exponential backoff with jitter**:

```js
function getReconnectDelay(attemptNumber) {
  const base = Math.min(1000 * 2 ** attemptNumber, 30000); // cap the exponential growth
  const jitter = Math.random() * base * 0.5; // randomize so clients don't all retry in lockstep
  return base + jitter;
}
```

Each failed attempt waits longer than the last (up to a cap), and the random jitter spreads simultaneous reconnection attempts out over time instead of letting them all land on the server in the same instant.

## 4. Message Framing at the Application Level

The WebSocket protocol frames *transport-level* messages (Part 1, §5) but has no opinion about the *content* of a message. Real applications need their own framing convention on top, typically a small JSON envelope distinguishing message types:

```js
// a simple application-level envelope convention
socket.send(JSON.stringify({ type: "chat_message", payload: { text: "hi" } }));
socket.send(JSON.stringify({ type: "typing_indicator", payload: { userId: 42 } }));

socket.onmessage = (event) => {
  const { type, payload } = JSON.parse(event.data);
  switch (type) {
    case "chat_message": /* ... */ break;
    case "typing_indicator": /* ... */ break;
    default: console.warn("unknown message type:", type);
  }
};
```

This matters for the same reason API versioning and request validation matter elsewhere in this curriculum: a long-lived connection will eventually need to evolve its message vocabulary, and an explicit `type` field (with a default/unknown-type fallback, as above) lets old and new clients coexist during a rollout rather than one side crashing on an unrecognized shape.

## Common Pitfalls

1. **Trusting a WebSocket connection just because the session cookie was valid, without checking `Origin`.** Per §1, this is a real, exploitable cross-origin hijack vector — WebSocket gives you none of CORS's automatic protection.
2. **Putting a long-lived, sensitive token directly in the connection URL as a query parameter.** It ends up in server logs and browser history; prefer a post-connect auth message or a short-lived, single-purpose token if a URL parameter is used at all.
3. **Reconnecting immediately and unconditionally on every `onclose`.** Without backoff and jitter, a server restart or blip becomes a self-inflicted denial-of-service from the client fleet's own reconnection storm.
4. **Assuming messages sent right before a disconnect were delivered.** There's no acknowledgment built into the protocol — an application that needs delivery guarantees must build its own ack/retry scheme on top, at the message-framing layer from §4.
5. **Skipping a `default` case in the message-type dispatcher.** An older client encountering a message `type` introduced after it shipped will throw or misbehave instead of gracefully ignoring what it doesn't understand.

## Quick Reference

```
WebSocket does NOT enforce same-origin — server MUST check the Origin header itself
  (cross-origin handshakes complete successfully by default, cookies included)

Auth options:
  cookie (subject to SameSite, needs Origin check per above)
  token in URL/subprotocol (logs/history risk — prefer short-lived tokens)
  post-connect auth message (keeps token out of URL, needs an enforced "unauthenticated" window)

Reconnection: exponential backoff + jitter — never immediate unconditional retry-in-a-loop

Message framing: WebSocket has no content opinion — applications define their own envelope,
  typically { type, payload } JSON, with an unknown-type fallback for forward compatibility

No built-in delivery guarantees across a reconnect — application-level acks if that matters
```
