From 1426d5be3328caa0d727c5d3dc03f24b04a4f114 Mon Sep 17 00:00:00 2001 From: sjaanus Date: Mon, 26 Sep 2022 09:58:58 +0200 Subject: [PATCH] Added login endpoint rate limit (#2074) * Added login rate limit * Make more pretty * Make more pretty * Fix * Remove double after all --- package.json | 1 + src/lib/routes/controller.ts | 5 ++ src/lib/routes/index.ts | 9 +- .../auth/simple-password-provider.e2e.test.ts | 85 +++++++++++++++++++ yarn.lock | 5 ++ 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/test/e2e/api/auth/simple-password-provider.e2e.test.ts diff --git a/package.json b/package.json index 9cd43720a3..c72b25aad3 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "deepmerge": "^4.2.2", "errorhandler": "^1.5.1", "express": "^4.17.1", + "express-rate-limit": "^6.6.0", "express-session": "^1.17.1", "fast-json-patch": "^3.1.0", "gravatar-url": "^3.1.0", diff --git a/src/lib/routes/controller.ts b/src/lib/routes/controller.ts index fc1f3ac485..3253120dc4 100644 --- a/src/lib/routes/controller.ts +++ b/src/lib/routes/controller.ts @@ -179,6 +179,11 @@ export default class Controller { this.app.use(path, router); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + useWithMiddleware(path: string, router: IRouter, middleware: any): void { + this.app.use(path, middleware, router); + } + get router(): any { return this.app; } diff --git a/src/lib/routes/index.ts b/src/lib/routes/index.ts index 4b4e345365..e0ba06f194 100644 --- a/src/lib/routes/index.ts +++ b/src/lib/routes/index.ts @@ -3,6 +3,7 @@ import ResetPasswordController from './auth/reset-password-controller'; import { SimplePasswordProvider } from './auth/simple-password-provider'; import { IUnleashConfig, IUnleashServices } from '../types'; import LogoutController from './logout'; +import rateLimit from 'express-rate-limit'; const AdminApi = require('./admin-api'); const ClientApi = require('./client-api'); @@ -19,9 +20,15 @@ class IndexRouter extends Controller { this.use('/health', new HealthCheckController(config, services).router); this.use('/internal-backstage', new BackstageController(config).router); this.use('/logout', new LogoutController(config, services).router); - this.use( + this.useWithMiddleware( '/auth/simple', new SimplePasswordProvider(config, services).router, + rateLimit({ + windowMs: 1 * 60 * 1000, + max: 5, + standardHeaders: true, + legacyHeaders: false, + }), ); this.use( '/auth/reset', diff --git a/src/test/e2e/api/auth/simple-password-provider.e2e.test.ts b/src/test/e2e/api/auth/simple-password-provider.e2e.test.ts new file mode 100644 index 0000000000..0b09d8babb --- /dev/null +++ b/src/test/e2e/api/auth/simple-password-provider.e2e.test.ts @@ -0,0 +1,85 @@ +import { createTestConfig } from '../../../config/test-config'; +import { IUnleashConfig } from '../../../../lib/types'; +import UserService from '../../../../lib/services/user-service'; +import { AccessService } from '../../../../lib/services/access-service'; +import { IUser } from '../../../../lib/types/user'; +import { setupApp } from '../../helpers/test-helper'; +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; +import { EmailService } from '../../../../lib/services/email-service'; +import SessionService from '../../../../lib/services/session-service'; +import { RoleName } from '../../../../lib/types/model'; +import SettingService from '../../../../lib/services/setting-service'; +import { GroupService } from '../../../../lib/services/group-service'; +import ResetTokenService from '../../../../lib/services/reset-token-service'; + +let app; +let stores; +let db; +const config: IUnleashConfig = createTestConfig({ + getLogger, + server: { + unleashUrl: 'http://localhost:3000', + baseUriPath: '', + }, + email: { + host: 'test', + }, +}); +const password = 'DtUYwi&l5I1KX4@Le'; +let userService: UserService; +let adminUser: IUser; + +beforeAll(async () => { + db = await dbInit('simple_password_provider_api_serial', getLogger); + stores = db.stores; + app = await setupApp(stores); + const groupService = new GroupService(stores, config); + const accessService = new AccessService(stores, config, groupService); + const resetTokenService = new ResetTokenService(stores, config); + const emailService = new EmailService(undefined, config.getLogger); + const sessionService = new SessionService(stores, config); + const settingService = new SettingService(stores, config); + + userService = new UserService(stores, config, { + accessService, + resetTokenService, + emailService, + sessionService, + settingService, + }); + const adminRole = await accessService.getRootRole(RoleName.ADMIN); + adminUser = await userService.createUser({ + username: 'admin@test.com', + email: 'admin@test.com', + rootRole: adminRole.id, + password: password, + }); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('Can log in', async () => { + await app.request + .post('/auth/simple/login') + .send({ + username: adminUser.username, + password, + }) + .expect(200); +}); + +test('Gets rate limited after 5 tries', async () => { + for (let statusCode of [200, 200, 200, 200, 429]) { + await app.request + .post('/auth/simple/login') + .send({ + username: adminUser.username, + password, + }) + .expect(statusCode); + } +}); diff --git a/yarn.lock b/yarn.lock index 5a93f4ee11..3dc05c2d82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3190,6 +3190,11 @@ expect@^29.0.0, expect@^29.0.1: jest-message-util "^29.0.1" jest-util "^29.0.1" +express-rate-limit@^6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-6.6.0.tgz#3bbc2546540d327b1b0bfa9ab5f1b2c49075af98" + integrity sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA== + express-session@^1.17.1: version "1.17.2" resolved "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz"