Skip to content

Commit 7342605

Browse files
authored
[Job Launcher][Server] Rate limiting (#3446)
1 parent 8d41eee commit 7342605

File tree

10 files changed

+95
-27
lines changed

10 files changed

+95
-27
lines changed

packages/apps/job-launcher/client/src/components/Auth/ForgotPasswordForm.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ export const ForgotPasswordForm = () => {
2626
const handleForgotPassword = async ({ email }) => {
2727
setIsLoading(true);
2828
try {
29-
await authService.forgotPassword(email);
29+
const hCaptchaToken = await captchaRef.current.getResponse();
30+
await authService.forgotPassword({
31+
email,
32+
hCaptchaToken,
33+
});
3034
setIsSuccess(true);
3135
} catch (err) {
3236
showError(err);

packages/apps/job-launcher/client/src/services/auth.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ForgotPasswordRequest,
23
ResetPasswordRequest,
34
SignInRequest,
45
SignUpRequest,
@@ -24,8 +25,8 @@ export const signOut = async (refreshToken: string) => {
2425
return data;
2526
};
2627

27-
export const forgotPassword = async (email: string) => {
28-
await api.post('/auth/forgot-password', { email });
28+
export const forgotPassword = async (body: ForgotPasswordRequest) => {
29+
await api.post('/auth/forgot-password', body);
2930
};
3031

3132
export const resetPassword = async (body: ResetPasswordRequest) => {

packages/apps/job-launcher/client/src/types/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export type SignUpResponse = {
1717
refreshToken: string;
1818
};
1919

20+
export type ForgotPasswordRequest = {
21+
email: string;
22+
hCaptchaToken: string;
23+
};
24+
2025
export type ResetPasswordRequest = {
2126
password: string;
2227
token: string;

packages/apps/job-launcher/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@nestjs/serve-static": "^4.0.1",
4444
"@nestjs/swagger": "^7.4.2",
4545
"@nestjs/terminus": "^11.0.0",
46+
"@nestjs/throttler": "^6.4.0",
4647
"@nestjs/typeorm": "^10.0.1",
4748
"@sendgrid/mail": "^8.1.3",
4849
"@types/passport-jwt": "^4.0.1",

packages/apps/job-launcher/server/src/app.module.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
23
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
3-
import { ServeStaticModule } from '@nestjs/serve-static';
44
import { ScheduleModule } from '@nestjs/schedule';
5-
import { ConfigModule } from '@nestjs/config';
5+
import { ServeStaticModule } from '@nestjs/serve-static';
6+
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
67
import { join } from 'path';
7-
88
import { AppController } from './app.controller';
9-
import { DatabaseModule } from './database/database.module';
9+
import { EnvConfigModule } from './common/config/config.module';
10+
import { envValidator } from './common/config/env-schema';
11+
import { ExceptionFilter } from './common/exceptions/exception.filter';
1012
import { JwtAuthGuard } from './common/guards';
13+
import { SnakeCaseInterceptor } from './common/interceptors/snake-case';
14+
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
1115
import { HttpValidationPipe } from './common/pipes';
12-
import { HealthModule } from './modules/health/health.module';
16+
import Environment from './common/utils/environment';
17+
import { DatabaseModule } from './database/database.module';
1318
import { AuthModule } from './modules/auth/auth.module';
14-
import { UserModule } from './modules/user/user.module';
19+
import { CronJobModule } from './modules/cron-job/cron-job.module';
20+
import { HealthModule } from './modules/health/health.module';
1521
import { JobModule } from './modules/job/job.module';
1622
import { PaymentModule } from './modules/payment/payment.module';
17-
import { Web3Module } from './modules/web3/web3.module';
18-
import { envValidator } from './common/config/env-schema';
23+
import { QualificationModule } from './modules/qualification/qualification.module';
24+
import { StatisticModule } from './modules/statistic/statistic.module';
1925
import { StorageModule } from './modules/storage/storage.module';
20-
import { CronJobModule } from './modules/cron-job/cron-job.module';
21-
import { SnakeCaseInterceptor } from './common/interceptors/snake-case';
26+
import { UserModule } from './modules/user/user.module';
27+
import { Web3Module } from './modules/web3/web3.module';
2228
import { WebhookModule } from './modules/webhook/webhook.module';
23-
import { EnvConfigModule } from './common/config/config.module';
24-
import { ExceptionFilter } from './common/exceptions/exception.filter';
25-
import { StatisticModule } from './modules/statistic/statistic.module';
26-
import { QualificationModule } from './modules/qualification/qualification.module';
27-
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
28-
import Environment from './common/utils/environment';
2929

3030
@Module({
3131
providers: [
@@ -49,8 +49,20 @@ import Environment from './common/utils/environment';
4949
provide: APP_FILTER,
5050
useClass: ExceptionFilter,
5151
},
52+
{
53+
provide: APP_GUARD,
54+
useClass: ThrottlerGuard,
55+
},
5256
],
5357
imports: [
58+
ThrottlerModule.forRoot({
59+
throttlers: [
60+
{
61+
ttl: 60000,
62+
limit: 1000,
63+
},
64+
],
65+
}),
5466
ScheduleModule.forRoot(),
5567
ConfigModule.forRoot({
5668
/**

packages/apps/job-launcher/server/src/modules/auth/auth.controller.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import {
1010
UseGuards,
1111
UseInterceptors,
1212
} from '@nestjs/common';
13-
1413
import {
1514
ApiBearerAuth,
1615
ApiBody,
1716
ApiOperation,
1817
ApiResponse,
1918
ApiTags,
2019
} from '@nestjs/swagger';
20+
import { Throttle } from '@nestjs/throttler';
2121
import { ErrorAuth } from '../../common/constants/errors';
2222
import { Public } from '../../common/decorators';
2323
import { ValidationError } from '../../common/errors';
@@ -144,6 +144,7 @@ export class AuthJwtController {
144144

145145
@Public()
146146
@HttpCode(204)
147+
@Throttle({ default: { limit: 3, ttl: 60000 } })
147148
@Post('/forgot-password')
148149
@ApiOperation({
149150
summary: 'Forgot Password',
@@ -162,8 +163,11 @@ export class AuthJwtController {
162163
status: 404,
163164
description: 'Not Found. Could not find the requested content.',
164165
})
165-
public async forgotPassword(@Body() data: ForgotPasswordDto): Promise<void> {
166-
await this.authService.forgotPassword(data);
166+
public async forgotPassword(
167+
@Body() data: ForgotPasswordDto,
168+
@Ip() ip: string,
169+
): Promise<void> {
170+
await this.authService.forgotPassword(data, ip);
167171
}
168172

169173
@Public()

packages/apps/job-launcher/server/src/modules/auth/auth.dto.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export class ForgotPasswordDto {
1313
@IsEmail()
1414
@Transform(({ value }: { value: string }) => value.toLowerCase())
1515
public email: string;
16+
17+
@ApiProperty({ name: 'h_captcha_token' })
18+
@IsString()
19+
public hCaptchaToken: string;
1620
}
1721

1822
export class SignInDto {

packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,21 +330,30 @@ describe('AuthService', () => {
330330
it('should throw NotFoundError if user is not found', () => {
331331
findByEmailMock.mockResolvedValue(null);
332332
expect(
333-
authService.forgotPassword({ email: '[email protected]' }),
333+
authService.forgotPassword({
334+
335+
hCaptchaToken: 'token',
336+
}),
334337
).rejects.toThrow(new NotFoundError(ErrorUser.NotFound));
335338
});
336339

337340
it('should throw ForbiddenError if user is not active', () => {
338341
userEntity.status = UserStatus.INACTIVE;
339342
findByEmailMock.mockResolvedValue(userEntity);
340343
expect(
341-
authService.forgotPassword({ email: '[email protected]' }),
344+
authService.forgotPassword({
345+
346+
hCaptchaToken: 'token',
347+
}),
342348
).rejects.toThrow(new ForbiddenError(ErrorUser.UserNotActive));
343349
});
344350

345351
it('should remove existing token if it exists', async () => {
346352
findTokenMock.mockResolvedValue(tokenEntity);
347-
await authService.forgotPassword({ email: '[email protected]' });
353+
await authService.forgotPassword({
354+
355+
hCaptchaToken: 'token',
356+
});
348357

349358
expect(tokenRepository.deleteOne).toHaveBeenCalled();
350359
});
@@ -353,7 +362,7 @@ describe('AuthService', () => {
353362
sendGridService.sendEmail = jest.fn();
354363
const email = '[email protected]';
355364

356-
await authService.forgotPassword({ email });
365+
await authService.forgotPassword({ email, hCaptchaToken: 'token' });
357366

358367
expect(sendGridService.sendEmail).toHaveBeenCalledWith(
359368
expect.objectContaining({

packages/apps/job-launcher/server/src/modules/auth/auth.service.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,23 @@ export class AuthService {
181181
return { accessToken, refreshToken: newRefreshTokenEntity.uuid };
182182
}
183183

184-
public async forgotPassword(data: ForgotPasswordDto): Promise<void> {
184+
public async forgotPassword(
185+
data: ForgotPasswordDto,
186+
ip?: string,
187+
): Promise<void> {
188+
if (
189+
!(
190+
await verifyToken(
191+
this.authConfigService.hcaptchaProtectionUrl,
192+
this.authConfigService.hCaptchaSiteKey,
193+
this.authConfigService.hCaptchaSecret,
194+
data.hCaptchaToken,
195+
ip,
196+
)
197+
).success
198+
) {
199+
throw new ForbiddenError(ErrorAuth.InvalidCaptchaToken);
200+
}
185201
const userEntity = await this.userRepository.findByEmail(data.email);
186202

187203
if (!userEntity) {

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4434,6 +4434,7 @@ __metadata:
44344434
"@nestjs/swagger": "npm:^7.4.2"
44354435
"@nestjs/terminus": "npm:^11.0.0"
44364436
"@nestjs/testing": "npm:^10.4.6"
4437+
"@nestjs/throttler": "npm:^6.4.0"
44374438
"@nestjs/typeorm": "npm:^10.0.1"
44384439
"@sendgrid/mail": "npm:^8.1.3"
44394440
"@types/bcrypt": "npm:^5.0.2"
@@ -6706,6 +6707,17 @@ __metadata:
67066707
languageName: node
67076708
linkType: hard
67086709

6710+
"@nestjs/throttler@npm:^6.4.0":
6711+
version: 6.4.0
6712+
resolution: "@nestjs/throttler@npm:6.4.0"
6713+
peerDependencies:
6714+
"@nestjs/common": ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
6715+
"@nestjs/core": ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
6716+
reflect-metadata: ^0.1.13 || ^0.2.0
6717+
checksum: 10c0/796134644e341aad4a403b7431524db97adc31ae8771fc1160a4694a24c295b7a3dd15abcb72b9ea3a0702247b929f501fc5dc74a3f30d915f2667a39ba5c5d7
6718+
languageName: node
6719+
linkType: hard
6720+
67096721
"@nestjs/typeorm@npm:^10.0.1":
67106722
version: 10.0.2
67116723
resolution: "@nestjs/typeorm@npm:10.0.2"

0 commit comments

Comments
 (0)