WebSockets & Real-Time Apps

Authentication in Socket.io

18 min Lesson 11 of 35

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:
  1. Requires a valid JWT token in the handshake
  2. Extracts user information from the token
  3. Rejects connections with expired tokens
  4. Tracks how many devices each user is connected from
  5. Allows administrators to disconnect specific users

Test with both valid and invalid tokens to verify authentication works correctly.