From 6a520f5b26a3e2ed6345721b30ff4e3d9bfa903d Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 27 Apr 2023 15:06:24 +0500 Subject: [PATCH 1/2] fix: use RateLimiterMemory instead of RateLimiterMongo --- README.md | 2 +- api/src/utils/rateLimiter.ts | 21 +++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a6a9e6a..64a210b 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = default: 100; # After this, access is blocked for an hour -# Store number for 90 days since first fail +# Store number for 24 days since first fail # Once a successful login is attempted, it resets MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = default: 10; diff --git a/api/src/utils/rateLimiter.ts b/api/src/utils/rateLimiter.ts index d8b3cb0..a6ec1b0 100644 --- a/api/src/utils/rateLimiter.ts +++ b/api/src/utils/rateLimiter.ts @@ -1,10 +1,9 @@ -import mongoose from 'mongoose' -import { RateLimiterMongo } from 'rate-limiter-flexible' +import { RateLimiterMemory } from 'rate-limiter-flexible' export class RateLimiter { private static instance: RateLimiter - private limiterSlowBruteByIP: RateLimiterMongo - private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMongo + private limiterSlowBruteByIP: RateLimiterMemory + private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMemory private maxWrongAttemptsByIpPerDay: number private maxConsecutiveFailsByUsernameAndIp: number @@ -19,19 +18,17 @@ export class RateLimiter { MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP ) - this.limiterSlowBruteByIP = new RateLimiterMongo({ - storeClient: mongoose.connection, + this.limiterSlowBruteByIP = new RateLimiterMemory({ keyPrefix: 'login_fail_ip_per_day', points: this.maxWrongAttemptsByIpPerDay, duration: 60 * 60 * 24, blockDuration: 60 * 60 * 24 // Block for 1 day }) - this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMongo({ - storeClient: mongoose.connection, + this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMemory({ keyPrefix: 'login_fail_consecutive_username_and_ip', points: this.maxConsecutiveFailsByUsernameAndIp, - duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail + duration: 60 * 60 * 24 * 24, // Store number for 24 days since first fail blockDuration: 60 * 60 // Block for 1 hour }) } @@ -60,8 +57,7 @@ export class RateLimiter { this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey) ]) - // NOTE: To make use of blockDuration option from RateLimiterMongo - // comparison in both following if statements should have greater than symbol + // NOTE: To make use of blockDuration option, comparison in both following if statements should have greater than symbol // otherwise, blockDuration option will not work // For more info see: https://github.com/animir/node-rate-limiter-flexible/wiki/Options#blockduration @@ -103,10 +99,11 @@ export class RateLimiter { if (rlRejected instanceof Error) { throw rlRejected } else { - // based upon the implementation of consume method of RateLimiterMongo + // based upon the implementation of consume method of RateLimiterMemory // we are sure that rlRejected will contain msBeforeNext // for further reference, // see https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#login-endpoint-protection + // or see https://github.com/animir/node-rate-limiter-flexible#ratelimiterres-object return Math.ceil(rlRejected.msBeforeNext / 1000) } } From 7df9588e664e9965e1b75cf9124fb9a77adc5e97 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 27 Apr 2023 16:26:43 +0500 Subject: [PATCH 2/2] chore: fixed specs --- api/src/routes/api/spec/web.spec.ts | 152 +++++++++++++--------------- 1 file changed, 71 insertions(+), 81 deletions(-) diff --git a/api/src/routes/api/spec/web.spec.ts b/api/src/routes/api/spec/web.spec.ts index 968bfc5..f5529aa 100644 --- a/api/src/routes/api/spec/web.spec.ts +++ b/api/src/routes/api/spec/web.spec.ts @@ -47,6 +47,77 @@ describe('web', () => { }) }) + describe('SASLogon/authorize', () => { + let csrfToken: string + let authCookies: string + + beforeAll(async () => { + ;({ csrfToken } = await getCSRF(app)) + + await userController.createUser(user) + + const credentials = { + username: user.username, + password: user.password + } + + ;({ authCookies } = await performLogin(app, credentials, csrfToken)) + }) + + afterAll(async () => { + const collections = mongoose.connection.collections + const collection = collections['users'] + await collection.deleteMany({}) + }) + + it('should respond with authorization code', async () => { + const res = await request(app) + .post('/SASLogon/authorize') + .set('Cookie', [authCookies].join('; ')) + .set('x-xsrf-token', csrfToken) + .send({ clientId }) + + expect(res.body).toHaveProperty('code') + }) + + it('should respond with Bad Request if CSRF Token is missing', async () => { + const res = await request(app) + .post('/SASLogon/authorize') + .set('Cookie', [authCookies].join('; ')) + .send({ clientId }) + .expect(400) + + expect(res.text).toEqual('Invalid CSRF token!') + expect(res.body).toEqual({}) + }) + + it('should respond with Bad Request if clientId is missing', async () => { + const res = await request(app) + .post('/SASLogon/authorize') + .set('Cookie', [authCookies].join('; ')) + .set('x-xsrf-token', csrfToken) + .send({}) + .expect(400) + + expect(res.text).toEqual(`"clientId" is required`) + expect(res.body).toEqual({}) + }) + + it('should respond with Forbidden if clientId is incorrect', async () => { + const res = await request(app) + .post('/SASLogon/authorize') + .set('Cookie', [authCookies].join('; ')) + .set('x-xsrf-token', csrfToken) + .send({ + clientId: 'WrongClientID' + }) + .expect(403) + + expect(res.text).toEqual('Error: Invalid clientId.') + expect(res.body).toEqual({}) + }) + }) + describe('SASLogon/login', () => { let csrfToken: string @@ -187,78 +258,6 @@ describe('web', () => { expect(res.body).toEqual({}) }) }) - - describe('SASLogon/authorize', () => { - let csrfToken: string - let authCookies: string - - beforeAll(async () => { - await deleteDocumentsFromLimitersCollections() - ;({ csrfToken } = await getCSRF(app)) - - await userController.createUser(user) - - const credentials = { - username: user.username, - password: user.password - } - - ;({ authCookies } = await performLogin(app, credentials, csrfToken)) - }) - - afterAll(async () => { - const collections = mongoose.connection.collections - const collection = collections['users'] - await collection.deleteMany({}) - }) - - it('should respond with authorization code', async () => { - const res = await request(app) - .post('/SASLogon/authorize') - .set('Cookie', [authCookies].join('; ')) - .set('x-xsrf-token', csrfToken) - .send({ clientId }) - - expect(res.body).toHaveProperty('code') - }) - - it('should respond with Bad Request if CSRF Token is missing', async () => { - const res = await request(app) - .post('/SASLogon/authorize') - .set('Cookie', [authCookies].join('; ')) - .send({ clientId }) - .expect(400) - - expect(res.text).toEqual('Invalid CSRF token!') - expect(res.body).toEqual({}) - }) - - it('should respond with Bad Request if clientId is missing', async () => { - const res = await request(app) - .post('/SASLogon/authorize') - .set('Cookie', [authCookies].join('; ')) - .set('x-xsrf-token', csrfToken) - .send({}) - .expect(400) - - expect(res.text).toEqual(`"clientId" is required`) - expect(res.body).toEqual({}) - }) - - it('should respond with Forbidden if clientId is incorrect', async () => { - const res = await request(app) - .post('/SASLogon/authorize') - .set('Cookie', [authCookies].join('; ')) - .set('x-xsrf-token', csrfToken) - .send({ - clientId: 'WrongClientID' - }) - .expect(403) - - expect(res.text).toEqual('Error: Invalid clientId.') - expect(res.body).toEqual({}) - }) - }) }) const getCSRF = async (app: Express) => { @@ -285,12 +284,3 @@ const extractCSRF = (text: string) => /