DEV Community

Mouloud hasrane
Mouloud hasrane

Posted on • Edited on

WebSocket Authentication in NestJS: Handling JWT and Guards

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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)