Securing Socket.io in NestJS with JWT and Passport: A Comprehensive Guide [Part 1 ]

ยท

6 min read

Securing Socket.io in NestJS with JWT and Passport: A Comprehensive Guide [Part 1 ]

Install the following packages in your nest js app

npm i --save @nestjs/websockets @nestjs/platform-socket.io

After installing the packages you need to initialize in main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
import { IoAdapter } from '@nestjs/platform-socket.io';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useWebSocketAdapter(new IoAdapter());
  await app.listen(3000);
}
bootstrap();

Create a socket.guard.ts file and paste the below code we are creating a guard that extends the AuthGuard from a @nest/passport

import { ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
import { Socket } from 'socket.io';
import { ResponseEnum } from 'src/constants/enum';

@Injectable()
export class JwtSocketGuard extends AuthGuard('jwt-socket') {
  constructor() {
    super();
  }

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const client: Socket = context.switchToWs().getClient<Socket>();
    const token: string | string[] = client.handshake.query.token;

    //Extract the token from query
    if (token === undefined) {
      throw new HttpException(
        ResponseEnum.UNAUTHORIZED,
        HttpStatus.UNAUTHORIZED,
      );
    }

    const authtoken: string = Array.isArray(token) ? token[0] : token;
    client.data.token = authtoken; // Set the token on the Socket object

    return super.canActivate(context);
  }
}

Here's a breakdown of the main functionality of the canActivate method in the JwtSocketGuard:

  1. Extract Token: It extracts the JWT token from the WebSocket handshake query. This token is typically sent by the client as a means of authentication.

  2. Token Validation: It checks if the extracted token is undefined. If it's undefined, it means that the client did not provide a token, so the WebSocket connection is unauthorized.

  3. Setting Token: If the token is defined, it sets the token on the Socket object. This can be useful for later stages of the application where the token may be needed for further processing.

  4. Call Parent Method: Finally, it calls the canActivate method of the parent class (in this case, AuthGuard) to perform any additional validation or checks required by the parent class.

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
import { Socket } from 'socket.io';
import { configCredentials } from 'src/config/config';

@Injectable()
export class JwtSocketStrategy extends PassportStrategy(
  Strategy,
  'jwt-socket',
) {
  constructor() {
    super({
      jwtFromRequest: JwtSocketStrategy.extractJWT,
      ignoreExpiration: false,
      secretOrKey: configCredentials.JWTSECRET,
    });
  }

  private static extractJWT(socket: Socket): string | null {
    if (socket.data.token) {
      return socket.data.token;
    }
    return null;
  }

  async validate(payload: any) {
    // Here, you can perform additional validation or processing of the payload
    // If everything is valid, return the payload or a custom user object
    console.log('๐Ÿš€ ~ JwtStrategy ~ validate ~ payload:', payload);
    return { id: payload.id, currentRole: payload.role };
  }
}

In the JwtSocketStrategy class, we're passing the 'jwt-socket' string as the second argument to the PassportStrategy constructor. This argument represents the name of the strategy.

By using the same name ('jwt-socket') in both the JwtSocketGuard and JwtSocketStrategy classes, you ensure that the JwtSocketGuard is correctly using the custom JwtSocketStrategy.

The constructor of the JwtSocketStrategy class initializes the superclass PassportStrategy with configuration options required for JWT authentication. Let's break down each option:

  • jwtFromRequest: This option specifies how the JWT token will be extracted from the incoming request. In this case, it's set to JwtSocketStrategy.extractJWT, which is a custom function defined within the class. This function extracts the JWT token from the socket.data.token property of the WebSocket connection. So, whenever a WebSocket connection is established, this function will be invoked to retrieve the JWT token from the socket data.

  • ignoreExpiration: This option determines whether to ignore the expiration of the JWT token. When set to false, which is the case here, the Passport strategy will automatically check if the token has expired and reject it if it has. This helps ensure that only valid and non-expired tokens are accepted.

  • secretOrKey: This option specifies the secret key or public key used to verify the JWT token's signature. It's set to configCredentials.JWTSECRET, which presumably holds the JWT secret key fetched from the application's configuration. This key is essential for validating the integrity of the token and ensuring it hasn't been tampered with.

  • For JWTSECRET create a file with any name and paste the following code. Remember you must need to ensure to use correct relative path

  •   export const configCredentials = {
        JWTSECRET: 'add your secret key here that is hard to decode',
      };
    

The validate method in the JwtSocketStrategy class is responsible for validating the JWT payload extracted from the WebSocket connection. Let's break down its functionality:

async validate(payload: any) {
    // Here, you can perform additional validation or processing of the payload
    // If everything is valid, return the payload or a custom user object
    console.log('๐Ÿš€ ~ JwtStrategy ~ validate ~ payload:', payload);
    return { id: payload.id, currentRole: payload.role };
}
  • Purpose: The validate method serves as the handler for validating the JWT payload extracted from the WebSocket connection. It receives the decoded payload as an argument.

  • Custom Validation Logic: Within the validate method, you have the flexibility to perform additional validation or processing of the payload based on your application's requirements. This could include checking the validity of certain fields, verifying user permissions, or any other custom validation logic necessary for your application's security and functionality.

  • Return Value: If the payload passes all validation checks and is deemed valid, you typically return the payload itself or a custom user object containing relevant information extracted from the payload. In the provided example, it returns an object containing the id and currentRole fields extracted from the payload.

  • Logging: The console.log statement included in the method is for logging purposes. It logs the payload to the console, allowing you to inspect the content of the payload during development and debugging phases.

The validate method in the JwtSocketStrategy class provides a hook for performing custom validation logic on the JWT payload extracted from WebSocket connections. It enables you to enforce additional security measures and tailor the authentication process to suit your application's requirements.

Optimizing Real-Time Communication with Event Emission in NestJS WebSocket Authentication

For emiting the events and sending the live messages create a socket folder and socket.gateway.ts and paste the following code.

import { UseGuards } from '@nestjs/common';
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server } from 'socket.io';
import { JwtSocketGuard } from 'src/auth/guard/socket.guard';
@UseGuards(JwtSocketGuard)
@WebSocketGateway(5000)
export class SocketGateway {
  @WebSocketServer() server: Server;

  @SubscribeMessage('events')
  handleEvent(@MessageBody() data: any): string {
    return data;
  }

  @SubscribeMessage('message')
  handleMessage(@MessageBody() message: string): void {
    console.log('๐Ÿš€ ~ SocketGateway ~ handleMessage ~ message:', message);
    this.server.emit('messages', message);
  }
}
  • UseGuards(JwtSocketGuard): This decorator from @nestjs/common is used to apply the JwtSocketGuard guard to the entire SocketGateway class. The JwtSocketGuard is responsible for authenticating WebSocket connections using JWT tokens.

  • @WebSocketGateway(5000): This decorator from @nestjs/websockets declares the class SocketGateway as a WebSocket gateway, listening on port 5000 by default. This means that WebSocket connections will be handled by this gateway.

  • @WebSocketServer() server: Server: This decorator and property combination injects the WebSocket server instance (Server) into the server property. This allows you to access the underlying socket.io server instance within your gateway.

  • @SubscribeMessage('events') and @SubscribeMessage('message'): These decorators specify WebSocket message handlers for the 'events' and 'message' events, respectively. These handlers are invoked when a client sends a message with the corresponding event name.

  • handleEvent(@MessageBody() data: any): This method handles WebSocket messages with the event name 'events'. It receives the message body (data) as an argument and returns it as a string.

  • handleMessage(@MessageBody() message: string): This method handles WebSocket messages with the event name 'message'. It receives the message body (message) as a string and logs it to the console. Additionally, it emits the message to all connected clients by using this.server.emit('messages', message).

Conclusion:

In this blog post, we've demonstrated how to implement secure WebSocket communication in a NestJS application using JWT and Passport for authentication. By applying guards and decorators provided by NestJS, you can easily secure your WebSocket endpoints and handle incoming messages efficiently.

ย