@mrzr/api-client

WebSockets & Socket.io

Authenticate WebSocket and socket.io connections with @mrzr/api-client using a short-lived socket ticket, without exposing the access token to the page.

A socket needs a credential too. There are two ways to give it one.

Add an endpoint that returns a short-lived, single-use ticket for the socket. api.getSocketToken(url) calls it as a normal authenticated request (token attached, refreshed on 401) and returns only the ticket. The access token never leaves the worker.

import { io } from "socket.io-client";

const socket = io("https://realtime.example.com", {
  // A function, so every reconnect gets a fresh ticket.
  auth: async (cb) => cb({ token: await api.getSocketToken("/auth/socket-ticket") }),
});

A plain WebSocket can't set headers, so pass the ticket in the URL:

const ticket = await api.getSocketToken("/auth/socket-ticket");
const ws = new WebSocket(`wss://realtime.example.com/?ticket=${encodeURIComponent(ticket)}`);

The endpoint is called with POST (pass { method: "GET" } to change that) and may return the ticket as a plain string or as { token }, { ticket } or { socketToken }, optionally inside { data }.

Opt-in: the access token itself

If the socket server only accepts your API token, enable exposeTokens:

const api = createClient({ baseUrl, exposeTokens: true });

const socket = io(URL, {
  auth: async (cb) => cb({ token: await api.getAccessToken() }),
});

getAccessToken() refreshes first when the token is about to expire. The trade-off: any script on the page can now read the token. Use the ticket when you can.

Reconnect when the session changes

api.onAuthStateChange((state) => {
  if (!state.isAuthenticated) socket.disconnect();
  else if (socket.connected) socket.disconnect().connect();   // fetches a new ticket
});

On this page