An unauthenticated Socket.io connection can listen to any room it joins and emit any event โ there is nothing stopping a user from joining any post room or emitting fake events unless you verify their identity. Just as Express protect middleware verifies JWTs on HTTP requests, Socket.io middleware verifies them on socket connections. In this lesson you will send the JWT in the socket auth handshake from React, write Socket.io middleware that calls jwt.verify() and attaches the decoded user to socket.data, and restrict room-join and event-emit operations to authenticated sockets.
The Socket Auth Handshake
React client โ sends token on connection:
io(SOCKET_URL, {
auth: { token: localStorage.getItem('token') }
})
Socket.io server โ middleware verifies token before connection:
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
const decoded = jwt.verify(token, JWT_SECRET);
socket.data.user = decoded; // attach user to socket
next(); // allow connection
} catch (err) {
next(new Error('Authentication failed')); // reject connection
}
});
After middleware:
socket.data.user = { id: '64a1f2b3', role: 'user', ... }
Use this in any socket event handler to identify the user
socket.data.user via a custom event, or disconnect expired sessions by checking token expiry in a long-lived timer.if (!socket.data.user) return socket.emit('error', 'auth required').socket.data.user from the client โ it is server-side data that you set after verifying the JWT, not something the client can modify. However, the token itself comes from the client and is verifiable only because it is signed by your JWT_SECRET. A token with an expired or invalid signature will be caught by jwt.verify() and the middleware will call next(error), rejecting the connection. Always use jwt.verify(), never jwt.decode(), in socket middleware.Socket.io Auth Middleware โ Server
// server/src/middleware/socketAuth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// โโ Full auth middleware โ fetches user from DB โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const socketAuthMiddleware = async (socket, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) {
// Allow unauthenticated connections (for public events like view counts)
socket.data.user = null;
return next();
}
// Verify the JWT
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Fetch the current user from DB โ ensures account still exists
const user = await User.findById(decoded.id).select('_id name role avatar');
if (!user) {
socket.data.user = null;
return next();
}
socket.data.user = user; // attach to socket for use in all handlers
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
// Allow connection but mark as unauthenticated
socket.data.user = null;
next();
} else {
// Invalid token โ reject
next(new Error('Authentication error'));
}
}
};
module.exports = socketAuthMiddleware;
// server/index.js โ apply middleware
const socketAuthMiddleware = require('./src/middleware/socketAuth');
io.use(socketAuthMiddleware);
Using socket.data.user in Event Handlers
// server/src/config/socket.js โ with auth-aware handlers
const setupSocketHandlers = (io) => {
io.on('connection', (socket) => {
const user = socket.data.user;
console.log(`Socket connected: ${socket.id} โ User: ${user?.name || 'anonymous'}`);
// โโ Join post room โ open to authenticated users only โโโโโโโโโโโโโโโโโ
socket.on('join-post', ({ postId }) => {
if (!user) return socket.emit('error', { message: 'Authentication required to join rooms' });
socket.join(`post:${postId}`);
socket.emit('joined-post', { postId });
});
// โโ Typing indicator โ require auth and use server-side user name โโโโ
socket.on('typing', ({ postId, isTyping }) => {
if (!user) return; // silently ignore unauthenticated typing events
// Use server-side user.name โ client cannot spoof it
socket.to(`post:${postId}`).emit('user-typing', {
userName: user.name,
isTyping,
});
});
// โโ Post a like โ require auth โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
socket.on('like-post', async ({ postId }) => {
if (!user) return socket.emit('error', { message: 'Login to like posts' });
// Update DB and broadcast
const post = await Post.findByIdAndUpdate(
postId,
{ $addToSet: { likes: user._id } },
{ new: true }
);
io.to(`post:${postId}`).emit('like-update', { likeCount: post.likes.length });
});
socket.on('disconnect', () => {
console.log(`Disconnected: ${socket.id}`);
});
});
};
module.exports = setupSocketHandlers;
React Client โ Sending Token in Auth Handshake
// src/hooks/useSocket.js โ with auth token
import { io } from 'socket.io-client';
import { useAuth } from '@/context/AuthContext';
const SOCKET_URL = import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:5000';
// โโ Token-aware socket factory โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let socketInstance = null;
const getSocket = (token) => {
// Disconnect and reconnect if token changes (login/logout)
if (socketInstance && socketInstance.auth?.token !== token) {
socketInstance.disconnect();
socketInstance = null;
}
if (!socketInstance) {
socketInstance = io(SOCKET_URL, {
auth: { token: token || '' }, // send token in handshake
autoConnect: true,
reconnection: true,
});
}
return socketInstance;
};
export function useSocket() {
const { token } = useAuth(); // { token } exposed by AuthContext
return getSocket(token);
}
// src/context/AuthContext.jsx โ expose token
const value = useMemo(() => ({
user,
token: localStorage.getItem('token'), // expose current token
login,
logout,
}), [user, login, logout]);
Handling Socket Auth Errors in React
// Listen for auth errors from the server
useEffect(() => {
const socket = getSocket(token);
socket.on('connect_error', (err) => {
if (err.message === 'Authentication error') {
console.error('Socket auth failed โ token may be invalid');
// Optionally logout the user
}
});
socket.on('error', ({ message }) => {
console.warn('Socket error:', message);
});
return () => {
socket.off('connect_error');
socket.off('error');
};
}, [token]);
Common Mistakes
Mistake 1 โ Using jwt.decode() instead of jwt.verify() in socket middleware
โ Wrong โ anyone can craft a socket with a fake decoded user ID:
const decoded = jwt.decode(token); // no signature check!
socket.data.user = { id: decoded.id }; // attacker sets id to admin's _id
โ Correct โ always verify the signature:
const decoded = jwt.verify(token, process.env.JWT_SECRET); // โ signature verified
Mistake 2 โ Trusting user data from the client in events
โ Wrong โ using client-provided userName in events:
socket.on('typing', ({ postId, userName }) => {
socket.to(`post:${postId}`).emit('user-typing', { userName }); // spoofable!
});
โ Correct โ use server-side socket.data.user:
socket.on('typing', ({ postId }) => {
const userName = socket.data.user?.name || 'Anonymous'; // โ server-side
socket.to(`post:${postId}`).emit('user-typing', { userName });
});
Mistake 3 โ Not reconnecting the socket after login/logout
โ Wrong โ socket created before login, new token not sent after authentication:
// Socket created anonymously, user logs in, socket still has no token
const socket = io(SOCKET_URL, { auth: { token: '' } });
// After login: socket.auth.token is still '' โ server sees unauthenticated
โ Correct โ disconnect and reconnect with the new token after login:
// After login in AuthContext:
socket.disconnect();
socketInstance = null; // clear cached instance
// Next getSocket(token) call creates a new connection with the fresh token โ
Quick Reference
| Task | Code |
|---|---|
| Send token from React | io(URL, { auth: { token } }) |
| Read token on server | socket.handshake.auth.token |
| Apply middleware | io.use((socket, next) => { ... next() }) |
| Attach user to socket | socket.data.user = user |
| Read user in handler | socket.data.user |
| Reject connection | next(new Error('message')) |
| Gate an event | if (!socket.data.user) return socket.emit('error', ...) |