Authentication in Socket.io
Securing WebSocket connections is critical for protecting sensitive data and preventing unauthorized access. Socket.io provides multiple authentication strategies to verify user identity before establishing connections.
Why Authenticate WebSocket Connections?
Unlike HTTP requests that can be authenticated per request, WebSocket connections are long-lived. Once established, they remain open, making initial authentication crucial:
- Prevent unauthorized access to real-time data streams
- Associate connections with specific users for personalized events
- Implement authorization rules for rooms and namespaces
- Track user activity and manage connections per user
Middleware Authentication
Socket.io middleware functions execute before the connection is established, allowing you to validate credentials and reject unauthorized connections:
<!-- Server-side (Node.js) -->
const io = require('socket.io')(server);
// Authentication middleware
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication token required'));
}
// Verify token (example with JWT)
try {
const decoded = verifyJWT(token);
socket.userId = decoded.userId;
socket.username = decoded.username;
next(); // Allow connection
} catch (err) {
next(new Error('Invalid token'));
}
});
io.on('connection', (socket) => {
console.log(`User ${socket.username} connected`);
});
Note: Middleware runs before the 'connection' event, giving you the opportunity to reject connections before they're established.
JWT Token Authentication
JSON Web Tokens (JWT) are a popular choice for WebSocket authentication because they're stateless and can carry user information:
// Server-side JWT verification
const jwt = require('jsonwebtoken');
const SECRET_KEY = process.env.JWT_SECRET;
function verifyJWT(token) {
return jwt.verify(token, SECRET_KEY);
}
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('No token provided'));
}
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return next(new Error('Invalid or expired token'));
}
socket.user = {
id: decoded.id,
username: decoded.username,
email: decoded.email,
role: decoded.role
};
next();
});
});
Client-Side Token Sending
The client must send authentication credentials during the handshake using the auth option:
<!-- Client-side -->
<script>
// Get token from localStorage or cookie
const token = localStorage.getItem('authToken');
const socket = io('http://localhost:3000', {
auth: {
token: token
}
});
// Handle authentication errors
socket.on('connect_error', (error) => {
if (error.message === 'Invalid token') {
alert('Authentication failed. Please log in again.');
// Redirect to login page
window.location.href = '/login';
}
});
</script>
Session-Based Authentication
If your application uses session cookies, you can authenticate WebSocket connections using existing sessions:
// Server-side with express-session
const session = require('express-session');
const sharedsession = require('express-socket.io-session');
// Express session middleware
const sessionMiddleware = session({
secret: 'my-secret',
resave: false,
saveUninitialized: false,
cookie: { secure: false }
});
app.use(sessionMiddleware);
// Share session with Socket.io
io.use(sharedsession(sessionMiddleware, {
autoSave: true
}));
io.use((socket, next) => {
const session = socket.handshake.session;
if (!session || !session.userId) {
return next(new Error('Not authenticated'));
}
socket.userId = session.userId;
socket.username = session.username;
next();
});
Tip: Session-based auth works well when WebSocket connections are on the same domain as your web application, as cookies are automatically sent.
Protecting Namespaces
Apply different authentication rules to different namespaces for granular access control:
// Public namespace (no auth required)
const publicIO = io.of('/public');
publicIO.on('connection', (socket) => {
console.log('Public connection');
});
// Admin namespace (requires admin role)
const adminIO = io.of('/admin');
adminIO.use((socket, next) => {
const token = socket.handshake.auth.token;
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err || decoded.role !== 'admin') {
return next(new Error('Admin access required'));
}
socket.user = decoded;
next();
});
});
adminIO.on('connection', (socket) => {
console.log(`Admin ${socket.user.username} connected`);
});
Disconnecting Unauthorized Users
You can disconnect users at any time if their authorization status changes:
io.on('connection', (socket) => {
// Verify user is still authorized
socket.on('adminAction', async (data) => {
const user = await getUserFromDatabase(socket.userId);
if (user.role !== 'admin') {
socket.emit('error', { message: 'Unauthorized action' });
socket.disconnect(true); // Force disconnect
return;
}
// Process admin action
});
});
// Disconnect user from all devices
function disconnectUser(userId) {
const sockets = await io.fetchSockets();
sockets.forEach(socket => {
if (socket.userId === userId) {
socket.emit('forceDisconnect', {
reason: 'Account suspended'
});
socket.disconnect(true);
}
});
}
Token Refresh Strategy
Handle token expiration gracefully by implementing a refresh mechanism:
<!-- Client-side token refresh -->
<script>
let socket;
function connectWithToken(token) {
socket = io('http://localhost:3000', {
auth: { token: token }
});
socket.on('connect_error', async (error) => {
if (error.message === 'Invalid or expired token') {
// Try to refresh token
const newToken = await refreshAuthToken();
if (newToken) {
socket.auth.token = newToken;
socket.connect(); // Retry connection
} else {
// Redirect to login
window.location.href = '/login';
}
}
});
}
async function refreshAuthToken() {
const response = await fetch('/api/refresh-token', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('authToken', data.token);
return data.token;
}
return null;
}
connectWithToken(localStorage.getItem('authToken'));
</script>
Warning: Never store sensitive tokens in URLs or query parameters. Always use the auth option or secure cookies.
Multi-Device Authentication
Track and manage multiple connections from the same user across different devices:
// Server-side connection tracking
const userConnections = new Map(); // userId => Set of socket IDs
io.on('connection', (socket) => {
const userId = socket.user.id;
// Add connection to user's set
if (!userConnections.has(userId)) {
userConnections.set(userId, new Set());
}
userConnections.get(userId).add(socket.id);
console.log(`User ${userId} has ${userConnections.get(userId).size} connections`);
socket.on('disconnect', () => {
userConnections.get(userId).delete(socket.id);
if (userConnections.get(userId).size === 0) {
userConnections.delete(userId);
console.log(`User ${userId} fully disconnected`);
}
});
});
// Send message to all user's devices
function sendToUser(userId, event, data) {
const connections = userConnections.get(userId);
if (connections) {
connections.forEach(socketId => {
io.to(socketId).emit(event, data);
});
}
}
Exercise: Create a WebSocket server with JWT authentication that:
- Requires a valid JWT token in the handshake
- Extracts user information from the token
- Rejects connections with expired tokens
- Tracks how many devices each user is connected from
- Allows administrators to disconnect specific users
Test with both valid and invalid tokens to verify authentication works correctly.