esc

Type to search...

First-message auth

Browser WebSockets cannot carry an Authorization header. Tokens in the URL end up in proxy and server logs. The cleaner pattern is to open the socket with no credentials, then send an auth frame as the very first message. The manager's onReady hook fires on every (re)connect, so this also handles reconnects for free.

Live Demo

First-message auth

idle
Authed
no
User
Token
Log

Waiting for the first event…

Simulate session expiry asks the server to declare the current session dead. The server emits auth-expired, the client mints a fresh token and sends a new auth frame on the same open socket — no reconnect.

Sign in with fresh token sends a new auth frame with a minted token. Use it to simulate a manual re-login.

Try bad token sends an auth frame the server will reject. The server replies unauthorized and the UI flips to the gated state.

Protocol

Three typed server messages cover the whole surface: auth-ok (the happy path), auth-expired (refresh, stay connected), and unauthorized (hard stop).

types.ts
type TClientMsg =
  | { type: "auth"; token: string }
  | { type: "simulate-session-expiry" };

type TServerMsg =
  | { type: "auth-required" }
  | { type: "auth-ok"; userId: string }
  | { type: "auth-expired" }
  | { type: "unauthorized"; reason: string };

Sending the first frame

onReady runs after the socket opens and subscriptions are replayed. On reconnects it runs again, so you do not need a separate effect to re-auth.

FirstMessageAuth.tsx
const manager = new WebSocketManager<TClientMsg, TServerMsg>({
  // No token in the URL. The socket opens unauthenticated.
  url: getWsUrl(),
  serialize: (msg) => JSON.stringify(msg),
  deserialize: (raw) => JSON.parse(raw),

  // Fires after every (re)connect + subscription replay.
  // Right seam for the first-message auth frame.
  onReady() {
    const token = tokenStore.current;
    if (!token) return;
    manager.send({ data: { type: "auth", token } });
  },
});

Reacting to server events

FirstMessageAuth.tsx
useSocketEvent(manager, "auth-expired", () => {
  setAuthed(false);
  // Refresh the token and send a new auth frame.
  // No reconnect: the socket stays open.
  const next = mintToken();
  tokenStore.current = next;
  manager.send({ data: { type: "auth", token: next } });
});

// msg is narrowed to { type: "auth-ok"; userId: string } — no cast
useSocketEvent(manager, "auth-ok", (msg) => {
  setAuthed(true);
  setUserId(msg.userId);
});

useSocketEvent(manager, "unauthorized", () => {
  setAuthed(false);
  setUserId(null);
});