From f97ae9ff73b3a20cf7187b4151cf850b59c594bf Mon Sep 17 00:00:00 2001 From: Christopher Graney-Ward Date: Sat, 13 Jul 2024 01:28:55 +0100 Subject: [PATCH 1/4] Fix the validate user function --- src/services/auth/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/auth/auth.service.ts b/src/services/auth/auth.service.ts index 00ba8ff..9327707 100644 --- a/src/services/auth/auth.service.ts +++ b/src/services/auth/auth.service.ts @@ -39,7 +39,7 @@ export class AuthService { if (!user.password) { throw new InternalServerErrorException('Email does not have associated password'); } - if (await bcrypt.compare(user.password, pass)) { + if (await bcrypt.compare(pass, user.password)) { const { password, ...result } = user; return result; } From db2c247d96d56abc64ea43a6189cdc9bb2366228 Mon Sep 17 00:00:00 2001 From: Christopher Graney-Ward Date: Sat, 13 Jul 2024 01:32:54 +0100 Subject: [PATCH 2/4] Refactor registration from controller to service --- src/services/auth/auth.controller.test.ts | 2 +- src/services/auth/auth.controller.ts | 9 +-------- src/services/auth/auth.service.ts | 14 +++++++++++++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/services/auth/auth.controller.test.ts b/src/services/auth/auth.controller.test.ts index 53ffd7f..47c6bf5 100644 --- a/src/services/auth/auth.controller.test.ts +++ b/src/services/auth/auth.controller.test.ts @@ -214,7 +214,7 @@ describe('AuthController', () => { const password = '!SUPER-SECRET_password!7'; // Create a user with the same email - await authService.createEmailAndPasswordUser(email, password, randomUUID()); + await authService.registerEmailAndPasswordUser(email, password); return request(app.getHttpServer()) .post('/v1/auth/register') diff --git a/src/services/auth/auth.controller.ts b/src/services/auth/auth.controller.ts index c9d563c..5505e90 100644 --- a/src/services/auth/auth.controller.ts +++ b/src/services/auth/auth.controller.ts @@ -44,7 +44,6 @@ import { RequestHiveAccountDto } from '../api/dto/RequestHiveAccount.dto'; import { HiveService } from '../hive/hive.service'; import { AuthInterceptor, UserDetailsInterceptor } from '../api/utils'; import { randomUUID } from 'crypto'; -import { v4 as uuid } from 'uuid'; import { EmailRegisterDto } from './dto/EmailRegister.dto'; @Controller('/v1/auth') @@ -340,17 +339,11 @@ export class AuthController { @ApiInternalServerErrorResponse({ description: 'Internal Server Error - unrelated to request body', }) - // @UseGuards(AuthGuard('local')) @Post('/register') async register(@Body() body: EmailRegisterDto) { const { email, password } = body; - const user_id = uuid(); - const email_code = await this.authService.createEmailAndPasswordUser(email, password, user_id); - - await this.emailService.sendRegistration(email, email_code); - - return await this.authService.login({ network: 'email', user_id }); + return await this.authService.registerEmailAndPasswordUser(email, password); } @ApiParam({ diff --git a/src/services/auth/auth.service.ts b/src/services/auth/auth.service.ts index 9327707..a75156f 100644 --- a/src/services/auth/auth.service.ts +++ b/src/services/auth/auth.service.ts @@ -16,6 +16,7 @@ import { LegacyUserRepository } from '../../repositories/user/user.repository'; import { LegacyHiveAccountRepository } from '../../repositories/hive-account/hive-account.repository'; import { DID } from 'dids'; import { ObjectId } from 'mongodb'; +import { EmailService } from '../email/email.service'; @Injectable() export class AuthService { @@ -25,6 +26,7 @@ export class AuthService { private readonly sessionRepository: SessionRepository, private readonly legacyHiveAccountRepository: LegacyHiveAccountRepository, private readonly jwtService: JwtService, + private readonly emailService: EmailService, ) {} jwtSign(payload: User) { @@ -136,7 +138,7 @@ export class AuthService { return await this.sessionRepository.findOneBySub(this.generateDidSub(did)); } - async createEmailAndPasswordUser( + async #createEmailAndPasswordUser( email: string, password: string, user_id: string, @@ -165,6 +167,16 @@ export class AuthService { } } + async registerEmailAndPasswordUser(email: string, password: string) { + const user_id = uuid(); + + const email_code = await this.#createEmailAndPasswordUser(email, password, user_id); + + await this.emailService.sendRegistration(email, email_code); + + return { access_token: this.jwtSign({ network: 'email', user_id }) }; + } + async createHiveUser({ user_id, hiveAccount }: { user_id: string; hiveAccount: string }) { const sub = this.generateHiveSub(hiveAccount); const account = await this.legacyUserRepository.createNewSubUser({ From 9ffd338093147d3028edf783b57121254f4d3dde Mon Sep 17 00:00:00 2001 From: Christopher Graney-Ward Date: Sat, 13 Jul 2024 01:38:42 +0100 Subject: [PATCH 3/4] Login endpoint tests and token generation --- src/services/auth/auth.controller.test.ts | 72 +++++++++++++++++++++++ src/services/auth/auth.controller.ts | 5 +- src/services/auth/auth.service.ts | 10 ++-- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/services/auth/auth.controller.test.ts b/src/services/auth/auth.controller.test.ts index 47c6bf5..b634dab 100644 --- a/src/services/auth/auth.controller.test.ts +++ b/src/services/auth/auth.controller.test.ts @@ -147,6 +147,78 @@ describe('AuthController', () => { }); }); + describe('/POST /login', () => { + it('Logs in successfully', async () => { + + const email = 'test@test.com'; + const password = 'testpass' + + await authService.registerEmailAndPasswordUser(email, password) + + // Make the request to the endpoint + return request(app.getHttpServer()) + .post('/v1/auth/login') + .send({ + username: email, + password: password + }) + .expect(201) + .then(async response => { + expect(response.body).toEqual({ + access_token: expect.any(String) + }); + }); + }); + + it('Throws unauthorized when the password is wrong', async () => { + + const email = 'test@test.com'; + const password = 'testpass' + + await authService.registerEmailAndPasswordUser(email, password) + + // Make the request to the endpoint + return request(app.getHttpServer()) + .post('/v1/auth/login') + .send({ + username: email, + password: password + 'im a hacker' + }) + .expect(401) + .then(async response => { + expect(response.body).toEqual({ + error: "Unauthorized", + message: "Email or password was incorrect", + statusCode: 401, + }); + }); + }); + + it('Throws when the email does not exist', async () => { + + const email = 'test@test.com'; + const password = 'testpass' + + await authService.registerEmailAndPasswordUser(email, password) + + // Make the request to the endpoint + return request(app.getHttpServer()) + .post('/v1/auth/login') + .send({ + username: 'different@email.com', + password: password + }) + .expect(401) + .then(async response => { + expect(response.body).toEqual({ + error: "Unauthorized", + message: "Email or password was incorrect", + statusCode: 401, + }); + }); + }); + }) + describe('/POST /request_hive_account', () => { it('creates a Hive account successfully', async () => { diff --git a/src/services/auth/auth.controller.ts b/src/services/auth/auth.controller.ts index 5505e90..696ce3e 100644 --- a/src/services/auth/auth.controller.ts +++ b/src/services/auth/auth.controller.ts @@ -66,9 +66,8 @@ export class AuthController { description: 'Login success', type: LoginResponseDto, }) - async login(@Request() req, @Body() body: LoginDto) { - const request = parseAndValidateRequest(req, this.#logger); - return this.authService.login(request.user); + async login(@Body() body: LoginDto): Promise { + return await this.authService.login(body.username); } //@UseGuards(AuthGuard('local')) diff --git a/src/services/auth/auth.service.ts b/src/services/auth/auth.service.ts index a75156f..ad59886 100644 --- a/src/services/auth/auth.service.ts +++ b/src/services/auth/auth.service.ts @@ -74,10 +74,12 @@ export class AuthService { return !!(await this.legacyUserRepository.findOneBySub(this.generateDidSub(did))); } - async login(user: User) { - return { - access_token: this.jwtSign(user), - }; + async login(email: string) { + const user = await this.legacyUserAccountRepository.findOneByEmail({ email }); + if (!user) { + throw new InternalServerErrorException('User was validated but cannot be found'); + } + return { access_token: this.jwtSign({ network: 'email', user_id: user.username }) }; } async getUserAccountBySub(sub: string) { From f44832c00183d057d17f36da5f14e7b9d83b50fb Mon Sep 17 00:00:00 2001 From: Christopher Graney-Ward Date: Wed, 31 Jul 2024 21:21:51 +0100 Subject: [PATCH 4/4] User must be email verified to log in --- src/services/auth/auth.controller.test.ts | 59 ++++++++++++++++++----- src/services/auth/auth.service.ts | 6 ++- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/services/auth/auth.controller.test.ts b/src/services/auth/auth.controller.test.ts index b634dab..7e2372a 100644 --- a/src/services/auth/auth.controller.test.ts +++ b/src/services/auth/auth.controller.test.ts @@ -17,8 +17,7 @@ import { DID } from 'dids'; import { Ed25519Provider } from 'key-did-provider-ed25519'; import { INestApplication, Module, ValidationPipe } from '@nestjs/common'; import * as KeyResolver from 'key-did-resolver'; -import { TestingModule } from '@nestjs/testing'; -import crypto, { randomUUID } from 'crypto'; +import crypto from 'crypto'; import { PrivateKey } from '@hiveio/dhive'; import { AuthGuard } from '@nestjs/passport'; import { MockAuthGuard, MockDidUserDetailsInterceptor, UserDetailsInterceptor } from '../api/utils'; @@ -27,13 +26,14 @@ import { HiveModule } from '../hive/hive.module'; import { LegacyUserRepository } from '../../repositories/user/user.repository'; import { EmailService } from '../email/email.service'; import { LegacyUserAccountRepository } from '../../repositories/userAccount/user-account.repository'; +import * as jest from 'jest-mock'; // Import Jest mock describe('AuthController', () => { - let app: INestApplication - const seedBuf = new Uint8Array(32) - seedBuf.fill(27) - const key = new Ed25519Provider(seedBuf) - const did = new DID({ provider: key, resolver: KeyResolver.getResolver() }) + let app: INestApplication; + const seedBuf = new Uint8Array(32); + seedBuf.fill(27); + const key = new Ed25519Provider(seedBuf); + const did = new DID({ provider: key, resolver: KeyResolver.getResolver() }); let mongod: MongoMemoryServer; let authService: AuthService; let hiveService: HiveService; @@ -41,6 +41,8 @@ describe('AuthController', () => { let emailService: EmailService; let userAccountRepository: LegacyUserAccountRepository; + let verificationCode: string; + beforeEach(async () => { mongod = await MongoMemoryServer.create(); const uri: string = mongod.getUri(); @@ -106,6 +108,12 @@ describe('AuthController', () => { userAccountRepository = moduleRef.get(LegacyUserAccountRepository); emailService = moduleRef.get(EmailService); + // Mocking the EmailService to capture the verification code + jest.spyOn(emailService, 'sendRegistration').mockImplementation(async (email, code) => { + verificationCode = code; // Store the verification code + return; + }); + app = moduleRef.createNestApplication(); app.useGlobalPipes(new ValidationPipe()); await app.init(); @@ -153,7 +161,8 @@ describe('AuthController', () => { const email = 'test@test.com'; const password = 'testpass' - await authService.registerEmailAndPasswordUser(email, password) + await authService.registerEmailAndPasswordUser(email, password); + await authService.verifyEmail(verificationCode); // Use the captured verification code // Make the request to the endpoint return request(app.getHttpServer()) @@ -175,7 +184,8 @@ describe('AuthController', () => { const email = 'test@test.com'; const password = 'testpass' - await authService.registerEmailAndPasswordUser(email, password) + await authService.registerEmailAndPasswordUser(email, password); + await authService.verifyEmail(verificationCode); // Make the request to the endpoint return request(app.getHttpServer()) @@ -188,7 +198,7 @@ describe('AuthController', () => { .then(async response => { expect(response.body).toEqual({ error: "Unauthorized", - message: "Email or password was incorrect", + message: "Email or password was incorrect or email has not been verified", statusCode: 401, }); }); @@ -199,7 +209,8 @@ describe('AuthController', () => { const email = 'test@test.com'; const password = 'testpass' - await authService.registerEmailAndPasswordUser(email, password) + await authService.registerEmailAndPasswordUser(email, password); + await authService.verifyEmail(verificationCode); // Make the request to the endpoint return request(app.getHttpServer()) @@ -212,7 +223,31 @@ describe('AuthController', () => { .then(async response => { expect(response.body).toEqual({ error: "Unauthorized", - message: "Email or password was incorrect", + message: "Email or password was incorrect or email has not been verified", + statusCode: 401, + }); + }); + }); + + it('Throws when the user has not verified their email', async () => { + + const email = 'test@test.com'; + const password = 'testpass' + + await authService.registerEmailAndPasswordUser(email, password); + + // Make the request to the endpoint + return request(app.getHttpServer()) + .post('/v1/auth/login') + .send({ + username: email, + password: password + }) + .expect(401) + .then(async response => { + expect(response.body).toEqual({ + error: "Unauthorized", + message: "Email or password was incorrect or email has not been verified", statusCode: 401, }); }); diff --git a/src/services/auth/auth.service.ts b/src/services/auth/auth.service.ts index 6b1e1ff..3f5a4c1 100644 --- a/src/services/auth/auth.service.ts +++ b/src/services/auth/auth.service.ts @@ -79,9 +79,11 @@ export class AuthService { } async login(email: string) { - const user = await this.legacyUserAccountRepository.findOneByEmail({ email }); + const user = await this.legacyUserAccountRepository.findOneVerifiedByEmail({ email }); if (!user) { - throw new InternalServerErrorException('User was validated but cannot be found'); + throw new InternalServerErrorException( + 'User was validated but cannot be found or has not verified their email', + ); } return { access_token: this.jwtSign({ network: 'email', user_id: user.username }) }; }