Learn WebSocket - Authentication & Authorization
Episode 8 of 34

Learn WebSocket - Authentication & Authorization

This episode secures WebSocket: authentication with JWT and sessions, carrying credentials during the handshake, Socket.IO middleware, and role-based and room-access authorization.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

The chat app from episode 7 accepts anyone who connects. In the real world that is unacceptable: you must know who is connected and limit what they are allowed to do. These are the two concepts that title this episode.

Episode 8 covers two layers: authentication to prove identity, and authorization to control access. You will carry a token during the handshake, verify it on the server, reject unauthorized connections, and control who may enter which room.

Authentication Strategies

Token-Based with JWT

The most common approach for WebSocket applications: the client already logs in via REST and receives a JWT. This token is then used to open the WebSocket connection.

JSCreating a JWT at login
const jwt = require("jsonwebtoken");
 
const token = jwt.sign(
  { userId: 42, role: "admin" },
  process.env.JWT_SECRET,
  { expiresIn: "1h" }
);

jwt.sign({ userId: 42 }, secret, { expiresIn: "1h" }) produces a token that carries identity claims. In production, keep JWT_SECRET in an environment variable, not in the code.

Other Alternatives

  • Session-based: the session ID is stored in an HttpOnly cookie; suits applications with traditional sessions.
  • Cookie-based: the client sends the cookie during the handshake; simple but risky if the cookie leaks.
  • Query parameter: the token via URL, easy but not recommended because the token lands in server logs and history.
  • Custom header: semantically clear, but custom headers cannot be set by browsers on WebSocket.

Authentication During the Handshake

Token in the Query String

The simplest way for browsers: carry the token as a URL parameter.

JSCarrying the token in the URL
const token = localStorage.getItem("token");
const ws = new WebSocket("wss://api.example.com/ws?token=" + token);

The URL wss://api.example.com/ws?token=... carries the token to the server. The server then reads it at the start of the connection. Because the token is visible in logs, make sure to use wss:// and short-lived tokens.

Verification on the ws Server

The server reads the query string and validates the token:

JSVerifying the token on the server
const { WebSocketServer } = require("ws");
const { URL } = require("url");
const jwt = require("jsonwebtoken");
const wss = new WebSocketServer({ port: 8080 });
 
wss.on("connection", (ws, req) => {
  const url = new URL(req.url, "http://localhost");
  const token = url.searchParams.get("token");
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    ws.userId = payload.userId;
    ws.role = payload.role;
    console.log("klien terautentikasi:", payload.userId);
  } catch {
    ws.close(1008, "unauthorized");
  }
});

jwt.verify(token, secret) throws an error if the token is invalid, and the handler calls ws.close(1008, "unauthorized") to reject the connection. Code 1008 means policy violation.

Socket.IO Authentication

Authentication Middleware

Socket.IO provides a dedicated channel: socket.handshake.auth for authentication data.

JSSocket.IO auth middleware
const { Server } = require("socket.io");
 
const io = new Server(server);
 
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    socket.userId = payload.userId;
    next();
  } catch {
    next(new Error("unauthorized"));
  }
});

The client sends the token via io("url", { auth: { token } }). The io.use(...) middleware validates before the connection is accepted — next(new Error("unauthorized")) rejects with a message the client can read.

Rejecting Connections Gracefully

JSCatching a rejected connection
const socket = io("https://api.example.com", {
  auth: { token: "token-buruk" },
});
 
socket.on("connect_error", (err) => {
  console.log("ditolak:", err.message);
});

The connect_error event gives you the reason for the rejection. This is far better than a connection that opens and then closes immediately without explanation.

Authorization

Roles and Permissions

Once the identity is known, set its access rights:

JSRole-based authorization
function bolehBuka(role, target) {
  const aturan = {
    admin: ["admin-room", "public-room"],
    user: ["public-room"],
  };
  return (aturan[role] || []).includes(target);
}
 
socket.on("join:room", (room) => {
  if (bolehBuka(socket.role, room)) {
    socket.join(room);
  } else {
    socket.emit("error", { kode: 403, pesan: "akses ditolak" });
  }
});

The bolehBuka(socket.role, room) function checks the list of rooms each role may access. Authorization policy is always evaluated on the server, never on the client.

Event-Level Authorization

Beyond room access, every event can be authorized:

JSPer-event authorization
socket.on("hapus:pengguna", (id) => {
  if (socket.role !== "admin") {
    socket.emit("error", { kode: 403 });
    return;
  }
  hapusPengguna(id);
});

Check socket.role !== "admin" before running sensitive operations. The principle stays consistent: validate at every door, not just the entrance.

Security and Best Practices

  • Never trust client data: roles and permissions always come from the verified token, not from client input.
  • Validate all input: messages and names must be sanitized before being broadcast.
  • Rate limiting: limit the message frequency per client — covered in episode 13.
  • Short-lived tokens: short JWTs, with a separate refresh token.
  • Store tokens securely: in the browser, prefer memory or an HttpOnly cookie over localStorage.

Closing

Episode 8 turned an open application into a controlled one: JWT authentication during the handshake, Socket.IO middleware, connection rejection, role- and room-based authorization, and basic security practices.

Key takeaways:

  • JWT is the most common authentication strategy for WebSocket.
  • Browsers cannot set custom headers; use a query string, cookie, or handshake auth.
  • Verify the token on the server, then close the connection with the right code.
  • The io.use middleware rejects connections before they get in.
  • Authorization is always evaluated on the server, per room and per event.
  • Short-lived tokens, validated input, and restricted access.

In the next episode we move into grouping: rooms, namespaces, and broadcasting patterns — joining and leaving rooms, separate namespaces for admins and the public, and targeted broadcast patterns.

Learn WebSocket - Authentication & Authorization | Learn WebSocket