From d94ffe6b63ae37c4da88ce3d06447c38bd71e0bc Mon Sep 17 00:00:00 2001 From: Shun Usami Date: Thu, 15 Feb 2024 21:44:01 -0800 Subject: [PATCH] :sparkles: Add 2FA disable endpoint --- backend/src/auth/auth.controller.ts | 14 ++++++++++++-- backend/src/auth/auth.service.ts | 15 +++++++++++++++ backend/test/auth.e2e-spec.ts | 11 +++++++++++ backend/test/utils/app.ts | 5 +++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 0e09a32b..31708d23 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Delete, Get, HttpCode, Post, @@ -16,14 +17,14 @@ import { ApiTags, } from '@nestjs/swagger'; import type { User } from '@prisma/client'; +import { Response } from 'express'; import { CurrentUser } from 'src/common/decorators/current-user.decorator'; import { AuthService } from './auth.service'; import { LoginDto } from './dto/login.dto'; import { TwoFactorAuthenticationDto } from './dto/twoFactorAuthentication.dto'; import { TwoFactorAuthenticationEnableDto } from './dto/twoFactorAuthenticationEnable.dto'; import { AuthEntity } from './entity/auth.entity'; -import { JwtGuardWithout2FA } from './jwt-auth.guard'; -import { Response } from 'express'; +import { JwtAuthGuard, JwtGuardWithout2FA } from './jwt-auth.guard'; const constants = { loginUrl: ((): string => { @@ -121,4 +122,13 @@ export class AuthController { ) { return this.authService.twoFactorAuthenticate(dto, user.id); } + + @Delete('2fa/disable') + @HttpCode(200) + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOkResponse() + async disable2FA(@CurrentUser() user: User) { + return this.authService.disableTwoFactorAuthentication(user.id); + } } diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index bb642124..dfc2299d 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -224,6 +224,21 @@ export class AuthService { }); } + disableTwoFactorAuthentication(userId: number) { + return this.prisma.$transaction(async (prisma) => { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user.twoFactorEnabled) { + throw new ConflictException('2FA secret is not enabled'); + } + await prisma.user.update({ + where: { id: user.id }, + data: { + twoFactorEnabled: false, + }, + }); + }); + } + async twoFactorAuthenticate(dto: TwoFactorAuthenticationDto, userId: number) { const user = await this.prisma.user.findUnique({ where: { id: userId } }); if (!user.twoFactorEnabled) { diff --git a/backend/test/auth.e2e-spec.ts b/backend/test/auth.e2e-spec.ts index bd694cc7..9f4566e2 100644 --- a/backend/test/auth.e2e-spec.ts +++ b/backend/test/auth.e2e-spec.ts @@ -77,5 +77,16 @@ describe('AuthController (e2e)', () => { it('[GET /user/me] should return 200 if 2FA is enabled and code is provided', async () => { await app.getMe(user.accessToken).expect(200); }); + + it('[DELETE /auth/2fa/disable] should disable 2FA', async () => { + await app.disableTwoFactorAuthentication(user.accessToken).expect(200); + }); + + it('[POST /auth/2fa/enable] should re-enable 2FA', async () => { + const code = authenticator.generate(secret); + await app + .enableTwoFactorAuthentication(code, user.accessToken) + .expect(200); + }); }); }); diff --git a/backend/test/utils/app.ts b/backend/test/utils/app.ts index b4a7c0a9..62f23f75 100644 --- a/backend/test/utils/app.ts +++ b/backend/test/utils/app.ts @@ -29,6 +29,11 @@ export class TestApp { .set('Authorization', `Bearer ${accessToken}`) .send({ code }); + disableTwoFactorAuthentication = (accessToken: string) => + request(this.app.getHttpServer()) + .delete('/auth/2fa/disable') + .set('Authorization', `Bearer ${accessToken}`); + twoFactorAuthenticate = (code: string, accessToken: string) => request(this.app.getHttpServer()) .post('/auth/2fa/authenticate')