Browse Source

Add rate limiting to authentication and sign up endpoints

pull/7263/head
Thomas Kaul 1 week ago
parent
commit
07be4ccf42
  1. 3
      README.md
  2. 24
      apps/api/src/app/app.module.ts
  3. 4
      apps/api/src/app/auth/auth.controller.ts
  4. 4
      apps/api/src/app/user/user.controller.ts
  5. 14
      apps/api/src/main.ts
  6. 1
      apps/api/src/services/configuration/configuration.service.ts
  7. 1
      apps/api/src/services/interfaces/environment.interface.ts
  8. 46
      package-lock.json
  9. 2
      package.json

3
README.md

@ -86,7 +86,7 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
### Supported Environment Variables
| Name | Type | Default Value | Description |
| --------------------------- | --------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens |
| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key |
| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key |
@ -106,6 +106,7 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
| `REDIS_PORT` | `number` | | The port where _Redis_ is running |
| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds |
| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. |
| `TRUST_PROXY` | `string` (optional) | | The [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting of _Express.js_ to determine the client IP address for rate limiting, e.g. `1` if the Ghostfolio application runs behind a single reverse proxy |
#### OpenID Connect OIDC (experimental)

24
apps/api/src/app/app.module.ts

@ -2,6 +2,7 @@ import { EventsModule } from '@ghostfolio/api/events/events.module';
import { BullBoardAuthMiddleware } from '@ghostfolio/api/middlewares/bull-board-auth.middleware';
import { HtmlTemplateMiddleware } from '@ghostfolio/api/middlewares/html-template.middleware';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CronModule } from '@ghostfolio/api/services/cron/cron.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
@ -18,13 +19,16 @@ import {
import { ExpressAdapter } from '@bull-board/express';
import { BullBoardModule } from '@bull-board/nestjs';
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
import { BullModule } from '@nestjs/bull';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
import { ThrottlerModule } from '@nestjs/throttler';
import { StatusCodes } from 'http-status-codes';
import ms from 'ms';
import { join } from 'node:path';
import { AccessModule } from './access/access.module';
@ -168,6 +172,26 @@ import { UserModule } from './user/user.module';
SubscriptionModule,
SymbolModule,
TagsModule,
ThrottlerModule.forRootAsync({
imports: [ConfigurationModule],
inject: [ConfigurationService],
useFactory: (configurationService: ConfigurationService) => {
return {
storage: new ThrottlerStorageRedisService({
db: configurationService.get('REDIS_DB'),
host: configurationService.get('REDIS_HOST'),
password: configurationService.get('REDIS_PASSWORD'),
port: configurationService.get('REDIS_PORT')
}),
throttlers: [
{
limit: 10,
ttl: ms('1 minute')
}
]
};
}
}),
UserModule,
WatchlistModule
],

4
apps/api/src/app/auth/auth.controller.ts

@ -22,6 +22,7 @@ import {
VERSION_NEUTRAL
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ThrottlerGuard } from '@nestjs/throttler';
import { Request, Response } from 'express';
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
@ -39,6 +40,7 @@ export class AuthController {
* @deprecated
*/
@Get('anonymous/:accessToken')
@UseGuards(ThrottlerGuard)
public async accessTokenLoginGet(
@Param('accessToken') accessToken: string
): Promise<OAuthResponse> {
@ -55,6 +57,7 @@ export class AuthController {
}
@Post('anonymous')
@UseGuards(ThrottlerGuard)
public async accessTokenLogin(
@Body() body: { accessToken: string }
): Promise<OAuthResponse> {
@ -156,6 +159,7 @@ export class AuthController {
}
@Post('webauthn/verify-authentication')
@UseGuards(ThrottlerGuard)
public async verifyAuthentication(
@Body() body: { deviceId: string; credential: AssertionCredentialJSON }
) {

4
apps/api/src/app/user/user.controller.ts

@ -37,9 +37,11 @@ import {
import { REQUEST } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from '@nestjs/passport';
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
import { User as UserModel } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { merge, size } from 'lodash';
import ms from 'ms';
import { UserService } from './user.service';
@ -128,6 +130,8 @@ export class UserController {
}
@Post()
@Throttle({ default: { limit: 5, ttl: ms('1 hour') } })
@UseGuards(ThrottlerGuard)
public async signupUser(): Promise<UserItem> {
const isUserSignupEnabled =
await this.propertyService.isUserSignupEnabled();

14
apps/api/src/main.ts

@ -97,6 +97,20 @@ async function bootstrap() {
});
}
const TRUST_PROXY = configService.get<string>('TRUST_PROXY');
if (TRUST_PROXY) {
let trustProxy: boolean | number | string = TRUST_PROXY;
if (/^\d+$/.test(TRUST_PROXY)) {
trustProxy = Number(TRUST_PROXY);
} else if (TRUST_PROXY === 'true') {
trustProxy = true;
}
app.set('trust proxy', trustProxy);
}
const HOST = configService.get<string>('HOST') || DEFAULT_HOST;
const PORT = configService.get<number>('PORT') || DEFAULT_PORT;

1
apps/api/src/services/configuration/configuration.service.ts

@ -114,6 +114,7 @@ export class ConfigurationService {
default: environment.rootUrl
}),
STRIPE_SECRET_KEY: str({ default: '' }),
TRUST_PROXY: str({ default: '' }),
TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }),
TWITTER_ACCESS_TOKEN_SECRET: str({ default: 'dummyAccessTokenSecret' }),
TWITTER_API_KEY: str({ default: 'dummyApiKey' }),

1
apps/api/src/services/interfaces/environment.interface.ts

@ -57,6 +57,7 @@ export interface Environment extends CleanedEnvAccessors {
REQUEST_TIMEOUT: number;
ROOT_URL: string;
STRIPE_SECRET_KEY: string;
TRUST_PROXY: string;
TWITTER_ACCESS_TOKEN: string;
TWITTER_ACCESS_TOKEN_SECRET: string;
TWITTER_API_KEY: string;

46
package-lock.json

@ -29,6 +29,7 @@
"@internationalized/number": "3.6.7",
"@ionic/angular": "8.8.12",
"@keyv/redis": "5.1.6",
"@nest-lab/throttler-storage-redis": "1.2.0",
"@nestjs/bull": "11.0.4",
"@nestjs/cache-manager": "3.1.3",
"@nestjs/common": "11.1.27",
@ -40,6 +41,7 @@
"@nestjs/platform-express": "11.1.27",
"@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0",
"@openrouter/ai-sdk-provider": "2.9.1",
"@prisma/adapter-pg": "7.8.0",
"@prisma/client": "7.8.0",
@ -6956,6 +6958,39 @@
"tslib": "^2.4.0"
}
},
"node_modules/@nest-lab/throttler-storage-redis": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@nest-lab/throttler-storage-redis/-/throttler-storage-redis-1.2.0.tgz",
"integrity": "sha512-tMkUyo68NCKTR+zILk+EC35SMYBtDPZY2mCj7ZaCietWGVTnuP4zwq9ERYfvU6kJv6h8teNZrC6MJCmY6/dljw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/throttler": ">=6.0.0",
"ioredis": ">=5.0.0",
"reflect-metadata": "^0.2.1"
},
"peerDependenciesMeta": {
"@nestjs/common": {
"optional": false
},
"@nestjs/core": {
"optional": false
},
"@nestjs/throttler": {
"optional": false
},
"ioredis": {
"optional": false
},
"reflect-metadata": {
"optional": false
}
}
},
"node_modules/@nestjs/bull": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-11.0.4.tgz",
@ -7441,6 +7476,17 @@
}
}
},
"node_modules/@nestjs/throttler": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz",
"integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0"
}
},
"node_modules/@ngtools/webpack": {
"version": "21.2.6",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-21.2.6.tgz",

2
package.json

@ -73,6 +73,7 @@
"@internationalized/number": "3.6.7",
"@ionic/angular": "8.8.12",
"@keyv/redis": "5.1.6",
"@nest-lab/throttler-storage-redis": "1.2.0",
"@nestjs/bull": "11.0.4",
"@nestjs/cache-manager": "3.1.3",
"@nestjs/common": "11.1.27",
@ -84,6 +85,7 @@
"@nestjs/platform-express": "11.1.27",
"@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5",
"@nestjs/throttler": "6.5.0",
"@openrouter/ai-sdk-provider": "2.9.1",
"@prisma/adapter-pg": "7.8.0",
"@prisma/client": "7.8.0",

Loading…
Cancel
Save