From 5887e3c0d098e0ca1f777eba238feeecde3a6c4c Mon Sep 17 00:00:00 2001 From: alexeh Date: Wed, 25 Sep 2024 07:08:12 +0200 Subject: [PATCH] refactor to use jwtManager --- .../modules/auth/authentication.controller.ts | 2 +- api/src/modules/auth/authentication.module.ts | 3 +- .../modules/auth/authentication.service.ts | 19 ++----- api/src/modules/auth/services/auth.mailer.ts | 35 +++--------- api/src/modules/auth/services/jwt.manager.ts | 53 ++++++++++++++++--- .../services/password-recovery.service.ts | 3 -- api/src/modules/users/users.controller.ts | 2 +- api/test/integration/auth/create-user.spec.ts | 2 +- api/test/integration/auth/sign-up.spec.ts | 2 +- api/test/utils/test-manager.ts | 2 +- 10 files changed, 62 insertions(+), 61 deletions(-) diff --git a/api/src/modules/auth/authentication.controller.ts b/api/src/modules/auth/authentication.controller.ts index 9b493d96..9a1f6e61 100644 --- a/api/src/modules/auth/authentication.controller.ts +++ b/api/src/modules/auth/authentication.controller.ts @@ -80,7 +80,7 @@ export class AuthenticationController { return tsRestHandler( authContract.validateToken, async ({ headers: { authorization }, query: { tokenType } }) => { - if (!(await this.authService.isTokenValid(authorization, tokenType))) { + if (!(await this.authService.verifyToken(authorization, tokenType))) { throw new UnauthorizedException(); } return { diff --git a/api/src/modules/auth/authentication.module.ts b/api/src/modules/auth/authentication.module.ts index bfdac3fb..83e4acea 100644 --- a/api/src/modules/auth/authentication.module.ts +++ b/api/src/modules/auth/authentication.module.ts @@ -47,7 +47,6 @@ import { JwtManager } from '@api/modules/auth/services/jwt.manager'; inject: [UsersService, ApiConfigService], }, ], - exports: [JwtModule, UsersModule, AuthenticationService, JwtManager], - // TODO: Remove JwtModule from exports + exports: [UsersModule, AuthenticationService, JwtManager], }) export class AuthenticationModule {} diff --git a/api/src/modules/auth/authentication.service.ts b/api/src/modules/auth/authentication.service.ts index 4908f685..df603b8a 100644 --- a/api/src/modules/auth/authentication.service.ts +++ b/api/src/modules/auth/authentication.service.ts @@ -1,12 +1,10 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; import { UsersService } from '@api/modules/users/users.service'; import { User } from '@shared/entities/users/user.entity'; import * as bcrypt from 'bcrypt'; import { CommandBus } from '@nestjs/cqrs'; import { UserWithAccessToken } from '@shared/dtos/user.dto'; import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; -import { ApiConfigService } from '@api/modules/config/app-config.service'; import { CreateUserDto } from '@shared/schemas/users/create-user.schema'; import { randomBytes } from 'node:crypto'; import { SendWelcomeEmailCommand } from '@api/modules/notifications/email/commands/send-welcome-email.command'; @@ -16,9 +14,7 @@ import { JwtManager } from '@api/modules/auth/services/jwt.manager'; export class AuthenticationService { constructor( private readonly usersService: UsersService, - private readonly jwt: JwtService, private readonly jwtManager: JwtManager, - private readonly apiConfig: ApiConfigService, private readonly commandBus: CommandBus, ) {} async validateUser(email: string, password: string): Promise { @@ -51,19 +47,10 @@ export class AuthenticationService { return { user, accessToken }; } - async isTokenValid(token: string, type: TOKEN_TYPE_ENUM): Promise { - const { secret } = this.apiConfig.getJWTConfigByType(type); - try { - const { id } = await this.jwt.verifyAsync(token, { secret }); - switch (type) { - case TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION: - return !(await this.usersService.isUserActive(id)); - default: - break; - } + async verifyToken(token: string, type: TOKEN_TYPE_ENUM): Promise { + if (await this.jwtManager.isTokenValid(token, type)) { return true; - } catch (error) { - return false; } + throw new UnauthorizedException(); } } diff --git a/api/src/modules/auth/services/auth.mailer.ts b/api/src/modules/auth/services/auth.mailer.ts index daa8806a..a646f98c 100644 --- a/api/src/modules/auth/services/auth.mailer.ts +++ b/api/src/modules/auth/services/auth.mailer.ts @@ -3,10 +3,8 @@ import { IEmailServiceInterface, IEmailServiceToken, } from '@api/modules/notifications/email/email-service.interface'; -import { ApiConfigService } from '@api/modules/config/app-config.service'; -import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; -import { JwtService } from '@nestjs/jwt'; import { User } from '@shared/entities/users/user.entity'; +import { JwtManager } from '@api/modules/auth/services/jwt.manager'; export type PasswordRecoveryDto = { user: User; @@ -18,18 +16,15 @@ export class AuthMailer { constructor( @Inject(IEmailServiceToken) private readonly emailService: IEmailServiceInterface, - private readonly apiConfig: ApiConfigService, - private readonly jwt: JwtService, + private readonly jwt: JwtManager, ) {} async sendPasswordRecoveryEmail( passwordRecovery: PasswordRecoveryDto, ): Promise { - const { token, expiresIn } = await this.signTokenByType( - TOKEN_TYPE_ENUM.RESET_PASSWORD, - passwordRecovery.user.id, - ); - const resetPasswordUrl = `${passwordRecovery.origin}/auth/forgot-password/${token}`; + const { resetPasswordToken, expiresIn } = + await this.jwt.signResetPasswordToken(passwordRecovery.user.id); + const resetPasswordUrl = `${passwordRecovery.origin}/auth/forgot-password/${resetPasswordToken}`; const htmlContent: string = `

Dear User,

@@ -56,14 +51,12 @@ export class AuthMailer { user: User; defaultPassword: string; }) { - const { token, expiresIn } = await this.signTokenByType( - TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION, - welcomeEmailDto.user.id, - ); + const { emailConfirmationToken, expiresIn } = + await this.jwt.signEmailConfirmationToken(welcomeEmailDto.user.id); // TODO: We need to know the URL to confirm the email, we could rely on origin but we would need to pass it through a lot of code. // probably better to have a config value for this. - const resetPasswordUrl = `TODO/auth/sign-up/${token}`; + const resetPasswordUrl = `TODO/auth/sign-up/${emailConfirmationToken}`; const htmlContent: string = `

Dear User,

@@ -86,18 +79,6 @@ export class AuthMailer { html: htmlContent, }); } - - private async signTokenByType( - tokenType: TOKEN_TYPE_ENUM, - userId: string, - ): Promise<{ token: string; expiresIn: string }> { - const { secret, expiresIn } = this.apiConfig.getJWTConfigByType(tokenType); - const token = await this.jwt.signAsync( - { id: userId }, - { secret, expiresIn }, - ); - return { token, expiresIn }; - } } const passwordRecoveryTokenExpirationHumanReadable = ( diff --git a/api/src/modules/auth/services/jwt.manager.ts b/api/src/modules/auth/services/jwt.manager.ts index 699d086b..b38dbe29 100644 --- a/api/src/modules/auth/services/jwt.manager.ts +++ b/api/src/modules/auth/services/jwt.manager.ts @@ -13,9 +13,19 @@ export class JwtManager { private readonly users: UsersService, ) {} - private sign(userId: string, tokenType: TOKEN_TYPE_ENUM): Promise { + private async sign( + userId: string, + tokenType: TOKEN_TYPE_ENUM, + ): Promise<{ token: string; expiresIn: string }> { const { secret, expiresIn } = this.config.getJWTConfigByType(tokenType); - return this.jwt.signAsync({ id: userId }, { secret, expiresIn }); + const token = await this.jwt.signAsync( + { id: userId }, + { secret, expiresIn }, + ); + return { + token, + expiresIn, + }; } private decode( @@ -26,34 +36,42 @@ export class JwtManager { return this.jwt.verifyAsync(token, { secret }); } - async signAccessToken(userId: string): Promise<{ accessToken: string }> { - const accessToken = await this.sign(userId, TOKEN_TYPE_ENUM.ACCESS); + async signAccessToken( + userId: string, + ): Promise<{ accessToken: string; expiresIn: string }> { + const { token: accessToken, expiresIn } = await this.sign( + userId, + TOKEN_TYPE_ENUM.ACCESS, + ); return { accessToken, + expiresIn, }; } async signResetPasswordToken( userId: string, - ): Promise<{ resetPasswordToken: string }> { - const resetPasswordToken = await this.sign( + ): Promise<{ resetPasswordToken: string; expiresIn: string }> { + const { token: resetPasswordToken, expiresIn } = await this.sign( userId, TOKEN_TYPE_ENUM.RESET_PASSWORD, ); return { resetPasswordToken, + expiresIn, }; } async signEmailConfirmationToken( userId: string, - ): Promise<{ emailConfirmationToken: string }> { - const emailConfirmationToken = await this.sign( + ): Promise<{ emailConfirmationToken: string; expiresIn: string }> { + const { token: emailConfirmationToken, expiresIn } = await this.sign( userId, TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION, ); return { emailConfirmationToken, + expiresIn, }; } @@ -68,4 +86,23 @@ export class JwtManager { async decodeEmailConfirmationToken(token: string): Promise { return this.decode(token, TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION); } + + async isTokenValid(token: string, type: TOKEN_TYPE_ENUM): Promise { + const { secret } = this.config.getJWTConfigByType(type); + try { + const { id } = await this.jwt.verifyAsync(token, { secret }); + switch (type) { + case TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION: + /** + * If the user is already active, we don't want to allow them to confirm their email again. + */ + return !(await this.users.isUserActive(id)); + default: + break; + } + return true; + } catch (error) { + return false; + } + } } diff --git a/api/src/modules/auth/services/password-recovery.service.ts b/api/src/modules/auth/services/password-recovery.service.ts index 394d1c04..f3df1478 100644 --- a/api/src/modules/auth/services/password-recovery.service.ts +++ b/api/src/modules/auth/services/password-recovery.service.ts @@ -1,11 +1,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { UsersService } from '@api/modules/users/users.service'; -import { JwtService } from '@nestjs/jwt'; import { AuthMailer } from '@api/modules/auth/services/auth.mailer'; import { EventBus } from '@nestjs/cqrs'; import { PasswordRecoveryRequestedEvent } from '@api/modules/events/user-events/password-recovery-requested.event'; -import { ApiConfigService } from '@api/modules/config/app-config.service'; -import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; import { User } from '@shared/entities/users/user.entity'; import * as bcrypt from 'bcrypt'; diff --git a/api/src/modules/users/users.controller.ts b/api/src/modules/users/users.controller.ts index 6dd3ad27..43b3842a 100644 --- a/api/src/modules/users/users.controller.ts +++ b/api/src/modules/users/users.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller } from '@nestjs/common'; @Controller('users') export class UsersController {} diff --git a/api/test/integration/auth/create-user.spec.ts b/api/test/integration/auth/create-user.spec.ts index 28d44e8d..2443d652 100644 --- a/api/test/integration/auth/create-user.spec.ts +++ b/api/test/integration/auth/create-user.spec.ts @@ -1,9 +1,9 @@ -import { ROLES } from '@api/modules/auth/authorisation/roles.enum'; import { TestManager } from '../../utils/test-manager'; import { User } from '@shared/entities/users/user.entity'; import { HttpStatus } from '@nestjs/common'; import { MockEmailService } from '../../utils/mocks/mock-email.service'; import { IEmailServiceToken } from '@api/modules/notifications/email/email-service.interface'; +import { ROLES } from '@api/modules/auth/roles.enum'; //create-user.feature diff --git a/api/test/integration/auth/sign-up.spec.ts b/api/test/integration/auth/sign-up.spec.ts index 7168c2d5..0a5a5cec 100644 --- a/api/test/integration/auth/sign-up.spec.ts +++ b/api/test/integration/auth/sign-up.spec.ts @@ -1,10 +1,10 @@ -import { ROLES } from '@api/modules/auth/authorisation/roles.enum'; import { TestManager } from '../../utils/test-manager'; import { HttpStatus } from '@nestjs/common'; import { ApiConfigService } from '@api/modules/config/app-config.service'; import { JwtService } from '@nestjs/jwt'; import { TOKEN_TYPE_ENUM } from '@shared/schemas/auth/token-type.schema'; import { authContract } from '@shared/contracts/auth.contract'; +import { ROLES } from '@api/modules/auth/roles.enum'; //create-user.feature diff --git a/api/test/utils/test-manager.ts b/api/test/utils/test-manager.ts index 7665e43a..aa7d49f3 100644 --- a/api/test/utils/test-manager.ts +++ b/api/test/utils/test-manager.ts @@ -13,7 +13,7 @@ import { createUser } from './mocks/entity-mocks'; import { User } from '@shared/entities/users/user.entity'; import { IEmailServiceToken } from '@api/modules/notifications/email/email-service.interface'; import { MockEmailService } from './mocks/mock-email.service'; -import { ROLES } from '@api/modules/auth/authorisation/roles.enum'; +import { ROLES } from '@api/modules/auth/roles.enum'; /** * @description: Abstraction for NestJS testing workflow. For now its a basic implementation to create a test app, but can be extended to encapsulate