Skip to content

Commit

Permalink
feat-update-auth-token-location (#1061)
Browse files Browse the repository at this point in the history
Signed-off-by: Svetoslav Borislavov <[email protected]>
  • Loading branch information
SvetBorislavov authored Oct 21, 2024
1 parent a486aef commit 6c0e8c7
Show file tree
Hide file tree
Showing 83 changed files with 1,601 additions and 1,206 deletions.
4 changes: 3 additions & 1 deletion back-end/apps/api/src/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
LoggerModule,
NotificationsProxyModule,
HealthModule,
BlacklistModule,
} from '@app/common';

import getEnvFilePaths from './config/envFilePaths';
Expand Down Expand Up @@ -62,6 +63,7 @@ export const config = ConfigModule.forRoot({
HealthModule,
IpThrottlerModule,
EmailThrottlerModule,
BlacklistModule.register({ isGlobal: true }),
],
providers: [
{
Expand All @@ -71,4 +73,4 @@ export const config = ConfigModule.forRoot({
LoggerMiddleware,
],
})
export class ApiModule {}
export class ApiModule {}
44 changes: 30 additions & 14 deletions back-end/apps/api/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import { UnprocessableEntityException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { mockDeep } from 'jest-mock-extended';
import { Request, Response } from 'express';
import { Request } from 'express';

import { guardMock } from '@app/common';
import { BlacklistService, guardMock } from '@app/common';
import { User, UserStatus } from '@entities';

import { AuthController } from './auth.controller';

import { AuthService } from './auth.service';

import { EmailThrottlerGuard } from '../guards';

jest.mock('passport-jwt', () => ({
ExtractJwt: {
fromAuthHeaderAsBearerToken: jest.fn(() => () => 'token'),
fromHeader: jest.fn(() => () => 'token'),
},
}));

describe('AuthController', () => {
let controller: AuthController;
let user: User;
let res: Response;

const authService = mockDeep<AuthService>();
const blacklistService = mockDeep<BlacklistService>();

const request: Request = {
protocol: 'http',
Expand All @@ -31,6 +39,10 @@ describe('AuthController', () => {
provide: AuthService,
useValue: authService,
},
{
provide: BlacklistService,
useValue: blacklistService,
},
],
})
.overrideGuard(EmailThrottlerGuard)
Expand All @@ -56,13 +68,13 @@ describe('AuthController', () => {
receivedNotifications: [],
notificationPreferences: [],
};
res = {
status: jest.fn().mockReturnThis(),
send: jest.fn(),
} as unknown as Response;
});

describe('signUp', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should return a user', async () => {
const result = user;

Expand Down Expand Up @@ -97,9 +109,9 @@ describe('AuthController', () => {

describe('login', () => {
it('should return a user', async () => {
const result = user;
await controller.login(user);

expect(await controller.login(user, res)).toBe(result);
expect(authService.login).toHaveBeenCalledWith(user);
});
});

Expand All @@ -120,23 +132,26 @@ describe('AuthController', () => {
it('should have no return value', async () => {
authService.createOtp.mockResolvedValue(undefined);

expect(await controller.createOtp({ email: '[email protected]' }, res)).toBeUndefined();
expect(await controller.createOtp({ email: '[email protected]' })).toBeUndefined();
});
});

describe('verify-reset', () => {
it('should have no return value', async () => {
authService.verifyOtp.mockResolvedValue(undefined);
const result = { token: 'newToken' };
authService.verifyOtp.mockResolvedValue(result);

expect(await controller.verifyOtp(user, { token: '' }, res)).toBeUndefined();
expect(await controller.verifyOtp(user, { token: '' }, request)).toEqual(result);
expect(blacklistService.blacklistToken).toHaveBeenCalledWith('token');
});
});

describe('set-password', () => {
it('should have no return value', async () => {
authService.setPassword.mockResolvedValue(undefined);

expect(await controller.setPassword(user, { password: 'Doe' }, res)).toBeUndefined();
expect(await controller.setPassword(user, { password: 'Doe' }, request)).toBeUndefined();
expect(blacklistService.blacklistToken).toHaveBeenCalledWith('token');
});
});

Expand All @@ -150,7 +165,8 @@ describe('AuthController', () => {

describe('logout', () => {
it('should have no return value', async () => {
expect(await controller.logout(res)).toBeUndefined();
await controller.logout(request);
expect(blacklistService.blacklistToken).toHaveBeenCalledWith('token');
});
});
});
73 changes: 37 additions & 36 deletions back-end/apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { Body, Controller, HttpCode, Patch, Post, Req, Res, UseGuards } from '@nestjs/common';
import { Body, Controller, HttpCode, Patch, Post, Req, UseGuards } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { SkipThrottle } from '@nestjs/throttler';

import { Request, Response } from 'express';
import { Request } from 'express';

import { Serialize } from '@app/common';
import { BlacklistService, Serialize } from '@app/common';

import { User } from '@entities';

import {
AdminGuard,
EmailThrottlerGuard,
extractJwtAuth,
extractJwtOtp,
JwtAuthGuard,
JwtBlackListAuthGuard,
JwtBlackListOtpGuard,
LocalAuthGuard,
OtpJwtAuthGuard,
OtpVerifiedAuthGuard,
Expand All @@ -24,20 +28,23 @@ import { AuthService } from './auth.service';

import {
AuthDto,
AuthenticateWebsocketTokenDto,
ChangePasswordDto,
LoginDto,
LoginResponseDto,
NewPasswordDto,
OtpDto,
OtpLocalDto,
SignUpUserDto,
AuthenticateWebsocketTokenDto,
} from './dtos';
import { UserDto } from '../users/dtos';

@ApiTags('Authentication')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
constructor(
private readonly authService: AuthService,
private readonly blacklistService: BlacklistService,
) {}

/* Register new users by admins */
@ApiOperation({
Expand All @@ -51,7 +58,7 @@ export class AuthController {
})
@Post('/signup')
@Serialize(AuthDto)
@UseGuards(JwtAuthGuard, AdminGuard, EmailThrottlerGuard)
@UseGuards(JwtBlackListAuthGuard, JwtAuthGuard, AdminGuard, EmailThrottlerGuard)
async signUp(@Body() dto: SignUpUserDto, @Req() req: Request): Promise<User> {
const url = `${req.protocol}://${req.get('host')}`;
return this.authService.signUpByAdmin(dto, url);
Expand All @@ -67,15 +74,15 @@ export class AuthController {
})
@ApiResponse({
status: 200,
description: 'User is verified and an authentication token in a cookie is attached.',
description: 'User is verified and an authentication token is returned along with the user.',
})
@Post('/login')
@HttpCode(200)
@UseGuards(LocalAuthGuard, EmailThrottlerGuard)
@Serialize(UserDto)
async login(@GetUser() user: User, @Res({ passthrough: true }) response: Response) {
await this.authService.login(user, response);
return user;
@Serialize(LoginResponseDto)
async login(@GetUser() user: User) {
const accessToken = await this.authService.login(user);
return { user, accessToken };
}

/* User log out */
Expand All @@ -85,13 +92,13 @@ export class AuthController {
})
@ApiResponse({
status: 200,
description: "The user's authentication token cookie is removed and added in the blacklist.",
description: "The user's authentication token is added in the blacklist.",
})
@Post('/logout')
@HttpCode(200)
@UseGuards(JwtAuthGuard)
logout(@Res({ passthrough: true }) response: Response) {
return this.authService.logout(response);
@UseGuards(JwtBlackListAuthGuard, JwtAuthGuard)
async logout(@Req() req: Request) {
await this.blacklistService.blacklistToken(extractJwtAuth(req));
}

/* Change user's password */
Expand All @@ -104,7 +111,7 @@ export class AuthController {
description: 'Password successfully changed.',
})
@Patch('/change-password')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtBlackListAuthGuard, JwtAuthGuard)
async changePassword(@GetUser() user: User, @Body() dto: ChangePasswordDto): Promise<void> {
return this.authService.changePassword(user, dto);
}
Expand All @@ -113,7 +120,7 @@ export class AuthController {
@ApiOperation({
summary: 'Request OTP for password reset',
description:
"Begin the process of resetting the user's password by creating and emailing an OTP to the user. A JWT cookie is attached to the response. Once the OTP is verified, the JWT cookie will be updated and the user will be able to set his new password.",
"Begin the process of resetting the user's password by creating and emailing an OTP to the user. A JWT is returned. Once the OTP is verified, a new JWT will be issued and the user will be able to set his new password.",
})
@ApiBody({
type: SignUpUserDto,
Expand All @@ -125,30 +132,28 @@ export class AuthController {
@Post('/reset-password')
@HttpCode(200)
@UseGuards(EmailThrottlerGuard)
async createOtp(@Body() { email }: OtpLocalDto, @Res({ passthrough: true }) response: Response) {
return this.authService.createOtp(email, response);
async createOtp(@Body() { email }: OtpLocalDto) {
return this.authService.createOtp(email);
}

/* Verify OTP for password reset */
@ApiOperation({
summary: 'Verify password reset',
description:
'Verify the user can reset the password by supplying the valid OTP. If the OTP is valid the JWT cookie is updated and the user will be able to set his new password',
'Verify the user can reset the password by supplying the valid OTP. If the OTP is valid , a new JWT is issued and the user will be able to set his new password',
})
@ApiResponse({
status: 200,
description:
'The OTP verified and the JWT cookie is updated. Now the user is able to set his new password. If the cookie is expired, the user will need to request a new OTP.',
'The OTP verified, new JWT is issued. Now the user is able to set his new password. If the JWT is expired, the user will need to request a new OTP.',
})
@Post('/verify-reset')
@HttpCode(200)
@UseGuards(OtpJwtAuthGuard)
verifyOtp(
@GetUser() user: User,
@Body() dto: OtpDto,
@Res({ passthrough: true }) response: Response,
) {
return this.authService.verifyOtp(user, dto, response);
@UseGuards(JwtBlackListOtpGuard, OtpJwtAuthGuard)
async verifyOtp(@GetUser() user: User, @Body() dto: OtpDto, @Req() req) {
const result = await this.authService.verifyOtp(user, dto);
await this.blacklistService.blacklistToken(extractJwtOtp(req));
return result;
}

/* Set the password for the user if the email has been verified */
Expand All @@ -160,15 +165,11 @@ export class AuthController {
status: 200,
description: 'Password successfully set.',
})
@UseGuards(OtpVerifiedAuthGuard)
@UseGuards(JwtBlackListOtpGuard, OtpVerifiedAuthGuard)
@Patch('/set-password')
async setPassword(
@GetUser() user: User,
@Body() dto: NewPasswordDto,
@Res({ passthrough: true }) response: Response,
): Promise<void> {
async setPassword(@GetUser() user: User, @Body() dto: NewPasswordDto, @Req() req): Promise<void> {
await this.authService.setPassword(user, dto.password);
this.authService.clearOtpCookie(response);
await this.blacklistService.blacklistToken(extractJwtOtp(req));
}

@SkipThrottle()
Expand Down
Loading

0 comments on commit 6c0e8c7

Please sign in to comment.