When working with WebSockets in NestJS using gateways, developers quickly encounter a fundamental challenge: traditional authentication guards don't work seamlessly with JWT tokens, especially short-lived ones. Unlike HTTP requests where each call can be independently authenticated, WebSocket connections are persistent, creating unique authentication requirements.
The Core Problem
NestJS guards are designed for request-response cycles, but WebSockets maintain long-lived connections. If you're using short-lived JWT tokens and attempt to verify authentication on each message, the token will eventually expire mid-connection, breaking the user experience. This leaves developers with a few viable approaches.
Guards Are more Usefull in Ws context With AuthZ (Authorization) like RBAC (message spececif)
Authentication Strategies
Session-Based Authentication
The most straightforward approach is using session-based authentication, where guards can function normally since sessions persist across the connection lifecycle. However, this approach may not suit all architectural requirements, particularly in stateless or distributed systems.
Long-Lived Token Method
One workaround is generating a long-lived token specifically for WebSocket connections during the initial handshake. This token gets attached to the client and acknowledged, allowing the connection to remain authenticated for its entire duration. While effective, this approach requires careful token management and potential security considerations around longer token lifespans.
Normal access tokens - refresh Tokens
This method is basically trying to recreate the flow of normal http access -refresh tokens jwt authentication , but Emitting a refresh token event to the server on Unauthorized Ws Exception and then waiting for an ack from the server with the new access and refresh tokens , this can work sure ! , but it can also cause problems at scale (parsing the jwt on each websocket message, connections management ...etc) , and requires more client side configuration.
Handshake-Only Verification
The most common approach is to verify authentication only during the WebSocket handshake. Once the connection is established and authenticated, subsequent messages are trusted. This balances security with practicality, though it requires careful connection management.
The Gateway Architecture Challenge
Here's where NestJS WebSocket implementation presents an interesting architectural challenge. When you have multiple gateways operating on the same namespace, any connection initialization logic defined in one gateway applies to all gateways in that namespace (this is not anywhere on the docs this was found with trial and error), and we are going to use that to in our favor to create a centralized Auth logic handler for our gatewatys
The Gateway Extension Pattern
The solution lies in creating a structured hierarchy of gateway classes that properly separate connection management from business logic.
Step 1: Create a Base WebSocket Manager
First, create a base gateway class (commonly named WSManager
or WebSocketManager
) that handles all connection-related logic:
export class WSManager implements OnGatewayInit, OnGatewayConnection {
//Use Property based injection on the parent class to avoid passing them on the sub-classes constructor
//class ref is the key of the provider change this if you have custom provider defenition
@Inject(AuthenticationService)
private readonly authService:AuthenticationService
constructor()
handleConnection(client: Socket, ...args: any[]) {
// Handle authentication logic here
// Verify JWT on handshake
const token=client.handshake.authentication.token
const user=this.authService.validateToken(token)
if(!user){
client.disconnect(true)
}
// Set up client metadata
client.user=user
}
afterInit(server: Server) {
// Server initialization logic
}
}
ps:you can add the @webSocketGateway() decorator to this class if you have the default namespace and you don't want to extend this on the gateways related to it
Step 2: Extend for Specific Functionality
For each specific WebSocket feature (notifications, chat, real-time updates, etc.), create dedicated gateways that extend the base manager:
so what will happen here is the notification Gateway will extend the WsManger implementation of the handleConnection from the WsManager
@WebSocketGateway({ namespace: '/notifications' })
export class NotificationGateway extends WSManager {
constructor(){
super()
}
@SubscribeMessage('subscribe-to-notifications')
handleNotificationSubscription(client: Socket, data: any) {
// Notification-specific logic
// Connection is already authenticated via parent class
}
}
Important Note
If you have a custom logic of the after Init other than Authentication Something like RBAC or Rooms joining logic , you can implement the OnGatewayConnection on the intended NameSpace while keeping the same logic as before by Calling
super.handleConnection()
@WebSocketGateway({ namespace: '/chat' })
export class NotificationGateway extends WSManager {
constructor(private readonly chatService:ChatService){
//Important here to call super for dependency injection to work
super()
}
handleConnection(client: any, ...args: any[]) {
super.handleConnection(client, ...args);
//Available now since it was set on the base Class
const user=client.user
const canConnect=this.chatService.canConnect(user)
if (!canConnect){
//Custom Exception
throw New ConnectionException('Cannot connect to the chat service')
}
const room=extractRoomfromHandShake(client.handshake)
client.join(room)
}
@SubscribeMessage('sendMessage')
handleSendMessage(client: Socket, data: any) {
// Connection is already authenticated via parent class and the user in the correct room(s)
}
}
Conclusion
The gateway extension pattern solves the fundamental challenge of WebSocket authentication in NestJS by separating connection management from business logic. This approach provides:
Centralized Authentication: All connection security logic in one reusable class
Clean Architecture: Business logic gateways focus on their specific domain
Maintainability: Changes to authentication flow only require updates in one location
Scalability: Easy to add new WebSocket features without duplicating authentication code
While NestJS WebSocket authentication presents unique challenges compared to traditional REST endpoints, understanding the connection lifecycle and leveraging proper inheritance patterns creates a robust, maintainable solution that scales with your application's needs.
Remember to always validate your authentication approach based on your specific security requirements and consider implementing additional measures like rate limiting and connection monitoring for production environments.
Top comments (0)