From 071845718132a2e55d3808f0579b52ec339d0389 Mon Sep 17 00:00:00 2001 From: erickmarx Date: Mon, 1 Jul 2024 14:19:15 -0300 Subject: [PATCH] feat: add jwt on ws connection --- src/modules/auth/auth.guard.ts | 12 ++-- src/modules/auth/auth.module.ts | 4 +- .../exceptions/ws-auth-business.exceptions.ts | 15 +++++ src/modules/auth/ws-auth.guard.ts | 59 +++++++++++++++++++ src/modules/chat/chat.gateway.ts | 11 +++- src/modules/chat/chat.module.ts | 7 ++- src/modules/chat/chat.service.ts | 14 +++-- .../ws-exception-filter.interface.ts | 12 ++++ 8 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 src/modules/auth/exceptions/ws-auth-business.exceptions.ts create mode 100644 src/modules/auth/ws-auth.guard.ts create mode 100644 src/shared/interceptor/ws-exception-filter.interface.ts diff --git a/src/modules/auth/auth.guard.ts b/src/modules/auth/auth.guard.ts index 073b6ac..b31805e 100644 --- a/src/modules/auth/auth.guard.ts +++ b/src/modules/auth/auth.guard.ts @@ -20,15 +20,15 @@ export class AuthGuard implements CanActivate { const isPublic = this.isPublic(context); - if (isPublic) return true; + if (!isPublic) { + const token = this.extractTokenFromHeader(request); - const token = this.extractTokenFromHeader(request); + const payload = await this.jwtStrategies.auth.verify(token); - const payload = await this.jwtStrategies.auth.verify(token); + await this.isEmailConfirmed(payload); - await this.isEmailConfirmed(payload); - - request['user'] = payload; + request['user'] = payload; + } return true; } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 7d0a093..0194f3d 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -8,6 +8,7 @@ import { JwtStrategies } from './jwt.strategies'; import { APP_GUARD } from '@nestjs/core'; import { AuthGuard } from './auth.guard'; import { ProfileModule } from '../profile/profile.module'; +import { WSAuthGuard } from './ws-auth.guard'; @Module({ imports: [UserModule, JwtModule, MailModule, ProfileModule], @@ -16,7 +17,8 @@ import { ProfileModule } from '../profile/profile.module'; AuthService, JwtStrategies, { provide: APP_GUARD, useClass: AuthGuard }, + WSAuthGuard, ], - exports: [JwtStrategies], + exports: [JwtStrategies, WSAuthGuard], }) export class AuthModule {} diff --git a/src/modules/auth/exceptions/ws-auth-business.exceptions.ts b/src/modules/auth/exceptions/ws-auth-business.exceptions.ts new file mode 100644 index 0000000..c50ad3f --- /dev/null +++ b/src/modules/auth/exceptions/ws-auth-business.exceptions.ts @@ -0,0 +1,15 @@ +import { WsException } from '@nestjs/websockets'; + +export class WSAuthBusinessExceptions { + static userNotFoundException() { + return new WsException('Usuario não encontrado'); + } + + static emailNotVerifiedException() { + return new WsException('Usuário não foi confirmado.'); + } + + static invalidTokenException() { + return new WsException('Token inválido.'); + } +} diff --git a/src/modules/auth/ws-auth.guard.ts b/src/modules/auth/ws-auth.guard.ts new file mode 100644 index 0000000..78e4eef --- /dev/null +++ b/src/modules/auth/ws-auth.guard.ts @@ -0,0 +1,59 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { JwtStrategies } from '~/modules/auth/jwt.strategies'; +import { JwtAuthPayload } from './interfaces/jwt-auth-payload.interface'; +import { UserService } from '../user/user.service'; +import { WSAuthBusinessExceptions } from './exceptions/ws-auth-business.exceptions'; +import { IGetConfirmed } from '../user/interfaces/get-confirmed.interface'; +import { Socket } from 'socket.io'; + +@Injectable() +export class WSAuthGuard implements CanActivate { + constructor( + private jwtStrategies: JwtStrategies, + private userService: UserService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const socket: Socket = context.switchToWs().getClient(); + + const token = this.extractTokenFromHeader(socket); + + let payload: JwtAuthPayload; + + try { + payload = await this.jwtStrategies.auth.verify(token); + } catch (e) { + throw WSAuthBusinessExceptions.invalidTokenException(); + } + + await this.isEmailConfirmed(payload); + + socket['user'] = payload; + + return true; + } + + private async isEmailConfirmed(payload: JwtAuthPayload): Promise { + let user: IGetConfirmed; + + try { + user = await this.userService.getConfirmedUser(payload.userId); + } catch (e) { + throw WSAuthBusinessExceptions.userNotFoundException(); + } + + if (!user.emailConfirmed) { + throw WSAuthBusinessExceptions.emailNotVerifiedException(); + } + } + + private extractTokenFromHeader(socket: Socket): string { + const [type, token] = + socket.handshake.headers.authorization?.split(' ') ?? []; + + if (!token || type !== 'Bearer') + throw WSAuthBusinessExceptions.invalidTokenException(); + + return token; + } +} diff --git a/src/modules/chat/chat.gateway.ts b/src/modules/chat/chat.gateway.ts index 228a87f..fbab3dc 100644 --- a/src/modules/chat/chat.gateway.ts +++ b/src/modules/chat/chat.gateway.ts @@ -7,24 +7,29 @@ import { IGatewayConnection } from './interface/gateway-connection.interface'; import { ISocket } from './interface/socket.interface'; import { ChatService } from './chat.service'; import { IServer } from './interface/server.interface'; +import { UseFilters, UseGuards } from '@nestjs/common'; +import { WSAuthGuard } from '../auth/ws-auth.guard'; +import { WSExceptionFilter } from '../../shared/interceptor/ws-exception-filter.interface'; @WebSocketGateway({ namespace: 'chat' }) +@UseGuards(WSAuthGuard) +@UseFilters(WSExceptionFilter) export class ChatGateway implements IGatewayConnection { @WebSocketServer() private server: IServer; constructor(private chatService: ChatService) {} - handleConnection(client: ISocket): void { + async handleConnection(client: ISocket): Promise { if (!this.server?.profiles) { this.server.profiles = new Map(); } - this.chatService.handleConnection(client, this.server); + await this.chatService.handleConnection(client, this.server); } handleDisconnect(client: ISocket): void { - this.chatService.handleConnection(client, this.server); + this.chatService.handleDisconnect(client, this.server); } @SubscribeMessage('health') diff --git a/src/modules/chat/chat.module.ts b/src/modules/chat/chat.module.ts index 39ae5b3..93616f0 100644 --- a/src/modules/chat/chat.module.ts +++ b/src/modules/chat/chat.module.ts @@ -1,6 +1,11 @@ import { Module } from '@nestjs/common'; import { ChatGateway } from './chat.gateway'; import { ChatService } from './chat.service'; +import { AuthModule } from '../auth/auth.module'; +import { UserModule } from '../user/user.module'; -@Module({ providers: [ChatGateway, ChatService] }) +@Module({ + imports: [UserModule, AuthModule], + providers: [ChatGateway, ChatService], +}) export class ChatModule {} diff --git a/src/modules/chat/chat.service.ts b/src/modules/chat/chat.service.ts index 7fcc7ca..d9d6cb8 100644 --- a/src/modules/chat/chat.service.ts +++ b/src/modules/chat/chat.service.ts @@ -1,15 +1,21 @@ import { Injectable } from '@nestjs/common'; import { ISocket } from './interface/socket.interface'; import { IServer } from './interface/server.interface'; +import { WsException } from '@nestjs/websockets'; +import { JwtStrategies } from '../auth/jwt.strategies'; @Injectable() export class ChatService { - handleConnection(client: ISocket, server: IServer) { - const auth = client.handshake.query.profile as string; + constructor(private jwtStrategies: JwtStrategies) {} - if (!auth) throw new Error('Unauthorized'); + async handleConnection(client: ISocket, server: IServer) { + const auth = client.handshake.headers.authorization?.split(' ')[1]; - client.profileId = auth; + if (!auth) throw new WsException('Unauthorized'); + + const { profileId } = await this.jwtStrategies.auth.verify(auth); + + client.profileId = profileId; server.profiles.set(client.profileId, client.id); } diff --git a/src/shared/interceptor/ws-exception-filter.interface.ts b/src/shared/interceptor/ws-exception-filter.interface.ts new file mode 100644 index 0000000..de1835d --- /dev/null +++ b/src/shared/interceptor/ws-exception-filter.interface.ts @@ -0,0 +1,12 @@ +import { Catch, ArgumentsHost } from '@nestjs/common'; +import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets'; + +@Catch(WsException) +export class WSExceptionFilter extends BaseWsExceptionFilter { + catch(exception: WsException, host: ArgumentsHost) { + const callback = host.getArgByIndex(2); + if (callback && typeof callback === 'function') { + callback({ exception: exception.message }); + } + } +}