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

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.
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.
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.
The simplest way for browsers: carry the token as a URL parameter.
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.
The server reads the query string and validates the token:
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 provides a dedicated channel: socket.handshake.auth for authentication data.
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.
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.
Once the identity is known, set its access rights:
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.
Beyond room access, every event can be authorized:
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.
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:
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.