diff --git a/packages/apps/reputation-oracle/server/src/common/config/auth-config.service.ts b/packages/apps/reputation-oracle/server/src/common/config/auth-config.service.ts index 2cca2a6bee..81c24aa0e1 100644 --- a/packages/apps/reputation-oracle/server/src/common/config/auth-config.service.ts +++ b/packages/apps/reputation-oracle/server/src/common/config/auth-config.service.ts @@ -61,4 +61,11 @@ export class AuthConfigService { 86400, ); } + + /** + * Human APP email. + */ + get humanAppEmail(): string { + return this.configService.getOrThrow('HUMAN_APP_EMAIL'); + } } diff --git a/packages/apps/reputation-oracle/server/src/common/config/env-schema.ts b/packages/apps/reputation-oracle/server/src/common/config/env-schema.ts index 1a1a8a05c7..98609817c7 100644 --- a/packages/apps/reputation-oracle/server/src/common/config/env-schema.ts +++ b/packages/apps/reputation-oracle/server/src/common/config/env-schema.ts @@ -69,4 +69,7 @@ export const envValidator = Joi.object({ KYC_API_KEY: Joi.string(), KYC_API_PRIVATE_KEY: Joi.string().required(), KYC_BASE_URL: Joi.string(), + + // Human App + HUMAN_APP_EMAIL: Joi.string().email().required(), }); diff --git a/packages/apps/reputation-oracle/server/src/common/constants/errors.ts b/packages/apps/reputation-oracle/server/src/common/constants/errors.ts index 44fc676bf0..cee20b71b5 100644 --- a/packages/apps/reputation-oracle/server/src/common/constants/errors.ts +++ b/packages/apps/reputation-oracle/server/src/common/constants/errors.ts @@ -52,14 +52,6 @@ export enum ErrorUser { DuplicatedAddress = 'The address you are trying to use already exists. Please check that the address is correct or use a different address.', } -/** - * Represents error messages related to captcha. - */ -export enum ErrorCapthca { - InvalidToken = 'Invalid captcha token provided', - VerificationFailed = 'Captcha verification failed', -} - /** * Represents error messages related to send grid. */ diff --git a/packages/apps/reputation-oracle/server/src/common/guards/hcaptcha.ts b/packages/apps/reputation-oracle/server/src/common/guards/hcaptcha.ts new file mode 100644 index 0000000000..72c5ef8209 --- /dev/null +++ b/packages/apps/reputation-oracle/server/src/common/guards/hcaptcha.ts @@ -0,0 +1,56 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + HttpStatus, + HttpException, + Logger, +} from '@nestjs/common'; +import { Request } from 'express'; +import { HCaptchaService } from '../../integrations/hcaptcha/hcaptcha.service'; +import { AuthConfigService } from '../config/auth-config.service'; + +@Injectable() +export class HCaptchaGuard implements CanActivate { + logger = new Logger(HCaptchaGuard.name); + constructor( + private readonly hCaptchaService: HCaptchaService, + private readonly authConfigSerice: AuthConfigService, + ) {} + public async canActivate(context: ExecutionContext): Promise { + const request: Request = context.switchToHttp().getRequest(); + + const { body } = request; + const hCaptchaToken = body['h_captcha_token']; + + // TODO: Remove 27-45 lines once we figure out how to replace human app user + if (request.path === '/auth/signin') { + const email = body['email']; + // Checking email here to avoid unnecessary db calls + if (email === this.authConfigSerice.humanAppEmail) { + return true; + } + } + + if (!hCaptchaToken) { + const message = 'hCaptcha token not provided'; + this.logger.error(message, request.path); + throw new HttpException( + { + message, + timestamp: new Date().toISOString(), + }, + HttpStatus.BAD_REQUEST, + ); + } + + const captchaVerificationResult = await this.hCaptchaService.verifyToken({ + token: hCaptchaToken, + }); + if (!captchaVerificationResult.success) { + throw new HttpException('Invalid hCaptcha token', HttpStatus.BAD_REQUEST); + } + + return true; + } +} diff --git a/packages/apps/reputation-oracle/server/src/modules/auth/auth.controller.ts b/packages/apps/reputation-oracle/server/src/modules/auth/auth.controller.ts index 9fa0ba1a90..2620fe40bf 100644 --- a/packages/apps/reputation-oracle/server/src/modules/auth/auth.controller.ts +++ b/packages/apps/reputation-oracle/server/src/modules/auth/auth.controller.ts @@ -32,6 +32,7 @@ import { } from './auth.dto'; import { AuthService } from './auth.service'; import { JwtAuthGuard } from '../../common/guards'; +import { HCaptchaGuard } from '../../common/guards/hcaptcha'; import { RequestWithUser } from '../../common/types'; import { TokenRepository } from './token.repository'; import { TokenType } from './token.entity'; @@ -66,6 +67,7 @@ export class AuthJwtController { @Public() @Post('/signup') + @UseGuards(HCaptchaGuard) @UseInterceptors(ClassSerializerInterceptor) @ApiOperation({ summary: 'User Signup', @@ -87,6 +89,7 @@ export class AuthJwtController { @Public() @Post('/signin') + @UseGuards(HCaptchaGuard) @HttpCode(200) @ApiOperation({ summary: 'User Signin', @@ -188,6 +191,7 @@ export class AuthJwtController { @Public() @Post('/forgot-password') + @UseGuards(HCaptchaGuard) @HttpCode(204) @ApiOperation({ summary: 'Forgot Password', @@ -212,6 +216,7 @@ export class AuthJwtController { @Public() @Post('/restore-password') + @UseGuards(HCaptchaGuard) @HttpCode(204) @ApiOperation({ summary: 'Restore Password', @@ -251,7 +256,7 @@ export class AuthJwtController { } @ApiBearerAuth() - @UseGuards(JwtAuthGuard) + @UseGuards(HCaptchaGuard, JwtAuthGuard) @HttpCode(204) @Post('/resend-email-verification') @ApiOperation({ diff --git a/packages/apps/reputation-oracle/server/src/modules/auth/auth.errors.ts b/packages/apps/reputation-oracle/server/src/modules/auth/auth.errors.ts index edf8e651e1..120c9b9594 100644 --- a/packages/apps/reputation-oracle/server/src/modules/auth/auth.errors.ts +++ b/packages/apps/reputation-oracle/server/src/modules/auth/auth.errors.ts @@ -1,4 +1,3 @@ -import { ErrorCapthca } from '../../common/constants/errors'; import { BaseError } from '../../common/errors/base'; export enum AuthErrorMessage { @@ -9,7 +8,7 @@ export enum AuthErrorMessage { } export class AuthError extends BaseError { - constructor(message: AuthErrorMessage | ErrorCapthca) { + constructor(message: AuthErrorMessage) { super(message); } } diff --git a/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.ts b/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.ts index 69faa8e82a..e6b71665d1 100644 --- a/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/auth/auth.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { ErrorCapthca } from '../../common/constants/errors'; import { OperatorStatus, Role as UserRole, @@ -65,14 +64,9 @@ export class AuthService { private readonly sendgridService: SendGridService, private readonly web3Service: Web3Service, private readonly userRepository: UserRepository, - private readonly hCaptchaService: HCaptchaService, ) {} - public async signin({ - email, - password, - hCaptchaToken, - }: SignInDto): Promise { + public async signin({ email, password }: SignInDto): Promise { const userEntity = await this.userRepository.findOneByEmail(email); if (!userEntity) { throw new AuthError(AuthErrorMessage.INVALID_CREDENTIALS); @@ -82,33 +76,10 @@ export class AuthService { throw new AuthError(AuthErrorMessage.INVALID_CREDENTIALS); } - if (userEntity.role !== UserRole.HUMAN_APP) { - if (!hCaptchaToken) { - throw new AuthError(ErrorCapthca.InvalidToken); - } - - const captchaVerificationResult = await this.hCaptchaService.verifyToken({ - token: hCaptchaToken, - }); - if (!captchaVerificationResult.success) { - throw new AuthError(ErrorCapthca.VerificationFailed); - } - } - return this.auth(userEntity); } public async signup(data: UserCreateDto): Promise { - if (!data.hCaptchaToken) { - throw new AuthError(ErrorCapthca.InvalidToken); - } - const captchaVerificationResult = await this.hCaptchaService.verifyToken({ - token: data.hCaptchaToken, - }); - if (!captchaVerificationResult.success) { - throw new AuthError(ErrorCapthca.VerificationFailed); - } - const storedUser = await this.userRepository.findOneByEmail(data.email); if (storedUser) { throw new DuplicatedUserError(data.email); @@ -237,16 +208,6 @@ export class AuthService { } public async forgotPassword(data: ForgotPasswordDto): Promise { - if (!data.hCaptchaToken) { - throw new AuthError(ErrorCapthca.InvalidToken); - } - const captchaVerificationResult = await this.hCaptchaService.verifyToken({ - token: data.hCaptchaToken, - }); - if (!captchaVerificationResult.success) { - throw new AuthError(ErrorCapthca.VerificationFailed); - } - const userEntity = await this.userRepository.findOneByEmail(data.email); if (!userEntity) { @@ -287,16 +248,6 @@ export class AuthService { } public async restorePassword(data: RestorePasswordDto): Promise { - if (!data.hCaptchaToken) { - throw new AuthError(ErrorCapthca.InvalidToken); - } - const captchaVerificationResult = await this.hCaptchaService.verifyToken({ - token: data.hCaptchaToken, - }); - if (!captchaVerificationResult.success) { - throw new AuthError(ErrorCapthca.VerificationFailed); - } - const tokenEntity = await this.tokenRepository.findOneByUuidAndType( data.token, TokenType.PASSWORD, @@ -347,16 +298,6 @@ export class AuthService { public async resendEmailVerification( data: ResendEmailVerificationDto, ): Promise { - if (!data.hCaptchaToken) { - throw new AuthError(ErrorCapthca.InvalidToken); - } - const captchaVerificationResult = await this.hCaptchaService.verifyToken({ - token: data.hCaptchaToken, - }); - if (!captchaVerificationResult.success) { - throw new AuthError(ErrorCapthca.VerificationFailed); - } - const userEntity = await this.userRepository.findOneByEmail(data.email); if (!userEntity || userEntity.status !== UserStatus.PENDING) { return; diff --git a/packages/apps/reputation-oracle/server/src/modules/user/user.controller.ts b/packages/apps/reputation-oracle/server/src/modules/user/user.controller.ts index 4e2fe18bd0..b8c08a7d33 100644 --- a/packages/apps/reputation-oracle/server/src/modules/user/user.controller.ts +++ b/packages/apps/reputation-oracle/server/src/modules/user/user.controller.ts @@ -27,6 +27,7 @@ import { RegistrationInExchangeOracleResponseDto, } from './user.dto'; import { JwtAuthGuard } from '../../common/guards'; +import { HCaptchaGuard } from '../../common/guards/hcaptcha'; import { RequestWithUser } from '../../common/types'; import { UserService } from './user.service'; import { Public } from '../../common/decorators'; @@ -35,12 +36,12 @@ import { KycSignedAddressDto } from '../kyc/kyc.dto'; @ApiTags('User') @Controller('/user') @ApiBearerAuth() -@UseGuards(JwtAuthGuard) export class UserController { constructor(private readonly userService: UserService) {} @Post('/register-labeler') @HttpCode(200) + @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'Register Labeler', description: 'Endpoint to register user as a labeler on hcaptcha services.', @@ -72,6 +73,7 @@ export class UserController { @Post('/register-address') @HttpCode(200) + @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'Register Blockchain Address', description: 'Endpoint to register blockchain address.', @@ -103,6 +105,7 @@ export class UserController { @Post('/enable-operator') @HttpCode(204) + @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'Enable an operator', description: 'Endpoint to enable an operator.', @@ -125,6 +128,7 @@ export class UserController { @Post('/disable-operator') @HttpCode(204) + @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'Disable an operator', description: 'Endpoint to disable an operator.', @@ -147,6 +151,7 @@ export class UserController { @Public() @Post('/prepare-signature') + @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'Web3 signature body', description: @@ -170,6 +175,7 @@ export class UserController { @Post('/exchange-oracle-registration') @HttpCode(200) + @UseGuards(HCaptchaGuard, JwtAuthGuard) @ApiOperation({ summary: 'Notifies registration in Exchange Oracle completed', description: @@ -193,13 +199,17 @@ export class UserController { @Req() request: RequestWithUser, @Body() data: RegistrationInExchangeOracleDto, ): Promise { - await this.userService.registrationInExchangeOracle(request.user, data); + await this.userService.registrationInExchangeOracle( + request.user, + data.oracleAddress, + ); return { oracleAddress: data.oracleAddress }; } @Get('/exchange-oracle-registration') @HttpCode(200) + @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'Retrieves Exchange Oracles the user is registered in', description: diff --git a/packages/apps/reputation-oracle/server/src/modules/user/user.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/user/user.service.spec.ts index ca10165c42..75f05289c9 100644 --- a/packages/apps/reputation-oracle/server/src/modules/user/user.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/user/user.service.spec.ts @@ -30,7 +30,6 @@ import { HCaptchaConfigService } from '../../common/config/hcaptcha-config.servi import { HttpService } from '@nestjs/axios'; import { ControlledError } from '../../common/errors/controlled'; import { - ErrorCapthca, ErrorOperator, ErrorSignature, ErrorUser, @@ -835,7 +834,7 @@ describe('UserService', () => { const result = await userService.registrationInExchangeOracle( userEntity as UserEntity, - oracleRegistration, + oracleRegistration.oracleAddress, ); expect(siteKeyRepository.createUnique).toHaveBeenCalledWith( @@ -874,41 +873,13 @@ describe('UserService', () => { const result = await userService.registrationInExchangeOracle( userEntity as UserEntity, - oracleRegistration, + oracleRegistration.oracleAddress, ); expect(siteKeyRepository.createUnique).not.toHaveBeenCalled(); expect(result).toEqual(siteKeyMock); }); - - it('should throw if captcha verification fails', async () => { - const userEntity: DeepPartial = { - id: 1, - email: 'test@example.com', - }; - - const oracleRegistration: RegistrationInExchangeOracleDto = { - oracleAddress: '0xOracleAddress', - hCaptchaToken: 'hcaptcha-token', - }; - - jest - .spyOn(hcaptchaService, 'verifyToken') - .mockResolvedValueOnce({ success: false }); - - await expect( - userService.registrationInExchangeOracle( - userEntity as UserEntity, - oracleRegistration, - ), - ).rejects.toThrow( - new ControlledError( - ErrorCapthca.VerificationFailed, - HttpStatus.BAD_REQUEST, - ), - ); - }); }); describe('getRegisteredOracles', () => { diff --git a/packages/apps/reputation-oracle/server/src/modules/user/user.service.ts b/packages/apps/reputation-oracle/server/src/modules/user/user.service.ts index 0a2ba26fd7..8da0e63bac 100644 --- a/packages/apps/reputation-oracle/server/src/modules/user/user.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/user/user.service.ts @@ -5,11 +5,7 @@ import { Logger, } from '@nestjs/common'; import * as bcrypt from 'bcrypt'; -import { - ErrorCapthca, - ErrorOperator, - ErrorUser, -} from '../../common/constants/errors'; +import { ErrorOperator, ErrorUser } from '../../common/constants/errors'; import { KycStatus, OperatorStatus, @@ -52,7 +48,6 @@ export class UserService { private readonly web3ConfigService: Web3ConfigService, private readonly hcaptchaConfigService: HCaptchaConfigService, private readonly networkConfigService: NetworkConfigService, - private readonly hCaptchaService: HCaptchaService, ) {} static checkPasswordMatchesHash( @@ -369,24 +364,8 @@ export class UserService { public async registrationInExchangeOracle( user: UserEntity, - { hCaptchaToken, oracleAddress }: RegistrationInExchangeOracleDto, + oracleAddress: string, ): Promise { - if (!hCaptchaToken) { - throw new ControlledError( - ErrorCapthca.InvalidToken, - HttpStatus.BAD_REQUEST, - ); - } - const captchaVerificationResult = await this.hCaptchaService.verifyToken({ - token: hCaptchaToken, - }); - if (!captchaVerificationResult.success) { - throw new ControlledError( - ErrorCapthca.VerificationFailed, - HttpStatus.BAD_REQUEST, - ); - } - const siteKey = await this.siteKeyRepository.findByUserSiteKeyAndType( user, oracleAddress,