Skip to content

Commit

Permalink
refactor to use jwtManager
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeh committed Sep 25, 2024
1 parent 5df31e3 commit 5887e3c
Show file tree
Hide file tree
Showing 10 changed files with 62 additions and 61 deletions.
2 changes: 1 addition & 1 deletion api/src/modules/auth/authentication.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions api/src/modules/auth/authentication.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
19 changes: 3 additions & 16 deletions api/src/modules/auth/authentication.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<User> {
Expand Down Expand Up @@ -51,19 +47,10 @@ export class AuthenticationService {
return { user, accessToken };
}

async isTokenValid(token: string, type: TOKEN_TYPE_ENUM): Promise<boolean> {
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<boolean> {
if (await this.jwtManager.isTokenValid(token, type)) {
return true;
} catch (error) {
return false;
}
throw new UnauthorizedException();
}
}
35 changes: 8 additions & 27 deletions api/src/modules/auth/services/auth.mailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<void> {
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 = `
<h1>Dear User,</h1>
Expand All @@ -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 = `
<h1>Dear User,</h1>
Expand All @@ -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 = (
Expand Down
53 changes: 45 additions & 8 deletions api/src/modules/auth/services/jwt.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ export class JwtManager {
private readonly users: UsersService,
) {}

private sign(userId: string, tokenType: TOKEN_TYPE_ENUM): Promise<string> {
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(
Expand All @@ -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,
};
}

Expand All @@ -68,4 +86,23 @@ export class JwtManager {
async decodeEmailConfirmationToken(token: string): Promise<JwtPayload> {
return this.decode(token, TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION);
}

async isTokenValid(token: string, type: TOKEN_TYPE_ENUM): Promise<boolean> {
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;
}
}
}
3 changes: 0 additions & 3 deletions api/src/modules/auth/services/password-recovery.service.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
2 changes: 1 addition & 1 deletion api/src/modules/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get } from '@nestjs/common';
import { Controller } from '@nestjs/common';

@Controller('users')
export class UsersController {}
2 changes: 1 addition & 1 deletion api/test/integration/auth/create-user.spec.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion api/test/integration/auth/sign-up.spec.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion api/test/utils/test-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 5887e3c

Please sign in to comment.