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
:
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.
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.
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.
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 toJwtSocketStrategy.extractJWT
, which is a custom function defined within the class. This function extracts the JWT token from thesocket.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 tofalse
, 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 toconfigCredentials.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 pathexport 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
andcurrentRole
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 theJwtSocketGuard
guard to the entireSocketGateway
class. TheJwtSocketGuard
is responsible for authenticating WebSocket connections using JWT tokens.@WebSocketGateway(5000)
: This decorator from@nestjs/websockets
declares the classSocketGateway
as a WebSocket gateway, listening on port5000
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 theserver
property. This allows you to access the underlyingsocket.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 usingthis.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.