diff --git a/README.md b/README.md index 1bdf184..766f5b1 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,19 @@ HELMET_COEP= # } HELMET_CSP_CONFIG_PATH=./csp.config.json +# To prevent brute force attack on login route we have implemented rate limiter +# Only valid for MODE: server +# Following are configurable env variable rate limiter + +# After this, access is blocked for 1 day +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 +# Once a successful login is attempted, it resets +MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = default: 10; + # LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common` # Docs: https://www.npmjs.com/package/morgan#predefined-formats LOG_FORMAT_MORGAN= diff --git a/api/.env.example b/api/.env.example index 58212f0..750f5d8 100644 --- a/api/.env.example +++ b/api/.env.example @@ -24,6 +24,12 @@ LDAP_BIND_PASSWORD = LDAP_USERS_BASE_DN = LDAP_GROUPS_BASE_DN = +#default value is 100 +MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100 + +#default value is 10 +MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10 + RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node diff --git a/api/package-lock.json b/api/package-lock.json index 20fe985..c38adab 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.2", "dependencies": { "@sasjs/core": "^4.40.1", - "@sasjs/utils": "2.48.1", + "@sasjs/utils": "3.2.0", "bcryptjs": "^2.4.3", "connect-mongo": "^4.6.0", "cookie-parser": "^1.4.6", @@ -24,6 +24,7 @@ "mongoose-sequence": "^5.3.1", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", + "rate-limiter-flexible": "2.4.1", "rotating-file-stream": "^3.0.4", "swagger-ui-express": "4.3.0", "unzipper": "^0.10.11", @@ -2027,6 +2028,24 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz", + "integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg==" + }, "node_modules/@hapi/hoek": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz", @@ -2543,17 +2562,17 @@ "integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ==" }, "node_modules/@sasjs/utils": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz", - "integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.2.0.tgz", + "integrity": "sha512-Hdt4t/ErAy9JeJAyH7sJ+tA3ipKYUwRAAWN1CGMG0+BK2/TUVjpPtP9xYCtKculzfHFadthNXTnFVTfe4D4MLw==", "hasInstallScript": true, "dependencies": { + "@fast-csv/format": "4.3.5", "@types/fs-extra": "9.0.13", "@types/prompts": "2.0.13", "chalk": "4.1.1", "cli-table": "0.3.6", "consola": "2.15.0", - "csv-stringify": "5.6.5", "find": "0.3.0", "fs-extra": "10.0.0", "jwt-decode": "3.1.2", @@ -2605,9 +2624,9 @@ } }, "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", @@ -4606,11 +4625,6 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, - "node_modules/csv-stringify": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", - "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==" - }, "node_modules/data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -8113,6 +8127,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -8123,11 +8142,26 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" + }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", @@ -9594,6 +9628,11 @@ "node": ">= 0.6" } }, + "node_modules/rate-limiter-flexible": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.4.1.tgz", + "integrity": "sha512-dgH4T44TzKVO9CLArNto62hJOwlWJMLUjVVr/ii0uUzZXEXthDNr7/yefW5z/1vvHAfycc1tnuiYyNJ8CTRB3g==" + }, "node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -12977,6 +13016,26 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "requires": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "14.18.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz", + "integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg==" + } + } + }, "@hapi/hoek": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz", @@ -13376,16 +13435,16 @@ "integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ==" }, "@sasjs/utils": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz", - "integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.2.0.tgz", + "integrity": "sha512-Hdt4t/ErAy9JeJAyH7sJ+tA3ipKYUwRAAWN1CGMG0+BK2/TUVjpPtP9xYCtKculzfHFadthNXTnFVTfe4D4MLw==", "requires": { + "@fast-csv/format": "4.3.5", "@types/fs-extra": "9.0.13", "@types/prompts": "2.0.13", "chalk": "4.1.1", "cli-table": "0.3.6", "consola": "2.15.0", - "csv-stringify": "5.6.5", "find": "0.3.0", "fs-extra": "10.0.0", "jwt-decode": "3.1.2", @@ -13427,9 +13486,9 @@ } }, "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" }, "@sideway/pinpoint": { "version": "2.0.0", @@ -15082,11 +15141,6 @@ } } }, - "csv-stringify": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", - "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==" - }, "data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -17708,6 +17762,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -17718,11 +17777,26 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, "lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" }, + "lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" + }, "lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", @@ -18811,6 +18885,11 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, + "rate-limiter-flexible": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.4.1.tgz", + "integrity": "sha512-dgH4T44TzKVO9CLArNto62hJOwlWJMLUjVVr/ii0uUzZXEXthDNr7/yefW5z/1vvHAfycc1tnuiYyNJ8CTRB3g==" + }, "raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", diff --git a/api/package.json b/api/package.json index 31782ab..2f07963 100644 --- a/api/package.json +++ b/api/package.json @@ -49,7 +49,7 @@ "author": "4GL Ltd", "dependencies": { "@sasjs/core": "^4.40.1", - "@sasjs/utils": "2.48.1", + "@sasjs/utils": "3.2.0", "bcryptjs": "^2.4.3", "connect-mongo": "^4.6.0", "cookie-parser": "^1.4.6", @@ -64,6 +64,7 @@ "mongoose-sequence": "^5.3.1", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", + "rate-limiter-flexible": "2.4.1", "rotating-file-stream": "^3.0.4", "swagger-ui-express": "4.3.0", "unzipper": "^0.10.11", diff --git a/api/src/controllers/web.ts b/api/src/controllers/web.ts index 9ed18ca..527cbe9 100644 --- a/api/src/controllers/web.ts +++ b/api/src/controllers/web.ts @@ -1,13 +1,14 @@ import path from 'path' import express from 'express' import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa' -import { readFile } from '@sasjs/utils' +import { readFile, convertSecondsToHms } from '@sasjs/utils' import User from '../model/User' import Client from '../model/Client' import { getWebBuildFolder, generateAuthCode, + RateLimiter, AuthProviderType, LDAPClient } from '../utils' @@ -83,19 +84,38 @@ const login = async ( ) => { // Authenticate User const user = await User.findOne({ username }) - if (!user) throw new Error('Username is not found.') - if ( - process.env.AUTH_PROVIDERS === AuthProviderType.LDAP && - user.authProvider === AuthProviderType.LDAP - ) { - const ldapClient = await LDAPClient.init() - await ldapClient.verifyUser(username, password) - } else { - const validPass = user.comparePassword(password) - if (!validPass) throw new Error('Invalid password.') + let validPass = false + + if (user) { + if ( + process.env.AUTH_PROVIDERS === AuthProviderType.LDAP && + user.authProvider === AuthProviderType.LDAP + ) { + const ldapClient = await LDAPClient.init() + validPass = await ldapClient + .verifyUser(username, password) + .catch(() => false) + } else { + validPass = user.comparePassword(password) + } } + // code to prevent brute force attack + + const rateLimiter = RateLimiter.getInstance() + + if (!validPass) { + const retrySecs = await rateLimiter.consume(req.ip, user?.username) + if (retrySecs > 0) throw errors.tooManyRequests(retrySecs) + } + + if (!user) throw errors.userNotFound + if (!validPass) throw errors.invalidPassword + + // Reset on successful authorization + rateLimiter.resetOnSuccess(req.ip, user.username) + req.session.loggedIn = true req.session.user = { userId: user.id, @@ -172,3 +192,18 @@ interface AuthorizeResponse { */ code: string } + +const errors = { + invalidPassword: { + code: 401, + message: 'Invalid Password.' + }, + userNotFound: { + code: 401, + message: 'Username is not found.' + }, + tooManyRequests: (seconds: number) => ({ + code: 429, + message: `Too Many Requests! Retry after ${convertSecondsToHms(seconds)}` + }) +} diff --git a/api/src/middlewares/bruteForceProtection.ts b/api/src/middlewares/bruteForceProtection.ts new file mode 100644 index 0000000..82edc1a --- /dev/null +++ b/api/src/middlewares/bruteForceProtection.ts @@ -0,0 +1,22 @@ +import { RequestHandler } from 'express' +import { convertSecondsToHms } from '@sasjs/utils' +import { RateLimiter } from '../utils' + +export const bruteForceProtection: RequestHandler = async (req, res, next) => { + const ip = req.ip + const username = req.body.username + + const rateLimiter = RateLimiter.getInstance() + + const retrySecs = await rateLimiter.check(ip, username) + + if (retrySecs > 0) { + res + .status(429) + .send(`Too Many Requests! Retry after ${convertSecondsToHms(retrySecs)}`) + + return + } + + next() +} diff --git a/api/src/middlewares/index.ts b/api/src/middlewares/index.ts index 209bda6..1b0423b 100644 --- a/api/src/middlewares/index.ts +++ b/api/src/middlewares/index.ts @@ -4,3 +4,4 @@ export * from './csrfProtection' export * from './desktop' export * from './verifyAdmin' export * from './verifyAdminIfNeeded' +export * from './bruteForceProtection' diff --git a/api/src/routes/api/spec/web.spec.ts b/api/src/routes/api/spec/web.spec.ts index c5ce0ca..968bfc5 100644 --- a/api/src/routes/api/spec/web.spec.ts +++ b/api/src/routes/api/spec/web.spec.ts @@ -82,6 +82,80 @@ describe('web', () => { }) }) + it('should respond with too many requests when attempting with invalid password for a same user too many times', async () => { + await userController.createUser(user) + + const promises: request.Test[] = [] + + const maxConsecutiveFailsByUsernameAndIp = Number( + process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP + ) + + Array(maxConsecutiveFailsByUsernameAndIp + 1) + .fill(0) + .map((_, i) => { + promises.push( + request(app) + .post('/SASLogon/login') + .set('x-xsrf-token', csrfToken) + .send({ + username: user.username, + password: 'invalid-password' + }) + ) + }) + + await Promise.all(promises) + + const res = await request(app) + .post('/SASLogon/login') + .set('x-xsrf-token', csrfToken) + .send({ + username: user.username, + password: user.password + }) + .expect(429) + + expect(res.text).toContain('Too Many Requests!') + }) + + it('should respond with too many requests when attempting with invalid credentials for different users but with same ip too many times', async () => { + await userController.createUser(user) + + const promises: request.Test[] = [] + + const maxWrongAttemptsByIpPerDay = Number( + process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY + ) + + Array(maxWrongAttemptsByIpPerDay + 1) + .fill(0) + .map((_, i) => { + promises.push( + request(app) + .post('/SASLogon/login') + .set('x-xsrf-token', csrfToken) + .send({ + username: `user${i}`, + password: 'invalid-password' + }) + ) + }) + + await Promise.all(promises) + + const res = await request(app) + .post('/SASLogon/login') + .set('x-xsrf-token', csrfToken) + .send({ + username: user.username, + password: user.password + }) + .expect(429) + + expect(res.text).toContain('Too Many Requests!') + }) + it('should respond with Bad Request if CSRF Token is not present', async () => { await userController.createUser(user) @@ -119,6 +193,7 @@ describe('web', () => { let authCookies: string beforeAll(async () => { + await deleteDocumentsFromLimitersCollections() ;({ csrfToken } = await getCSRF(app)) await userController.createUser(user) @@ -210,3 +285,12 @@ const extractCSRF = (text: string) => /