mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 19:44:35 +00:00
feat: prevent brute force attack by rate limiting login endpoint
This commit is contained in:
12
README.md
12
README.md
@@ -175,6 +175,18 @@ HELMET_COEP=
|
|||||||
# }
|
# }
|
||||||
HELMET_CSP_CONFIG_PATH=./csp.config.json
|
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
|
||||||
|
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
|
||||||
|
# After this, access is blocked for 1 day
|
||||||
|
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
|
||||||
|
# After this, access is blocked for an hour
|
||||||
|
# Store number for 90 days since first fail
|
||||||
|
# Once a successful login is attempted, it resets
|
||||||
|
|
||||||
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
||||||
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
||||||
LOG_FORMAT_MORGAN=
|
LOG_FORMAT_MORGAN=
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ LDAP_BIND_PASSWORD = <password>
|
|||||||
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
||||||
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
||||||
|
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=[100] default value is 100
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=[10] default value is 10
|
||||||
|
|
||||||
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
|
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
|
|||||||
11
api/package-lock.json
generated
11
api/package-lock.json
generated
@@ -24,6 +24,7 @@
|
|||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"rate-limiter-flexible": "2.4.1",
|
||||||
"rotating-file-stream": "^3.0.4",
|
"rotating-file-stream": "^3.0.4",
|
||||||
"swagger-ui-express": "4.3.0",
|
"swagger-ui-express": "4.3.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
@@ -9594,6 +9595,11 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/raw-body": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||||
@@ -18811,6 +18817,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
"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": {
|
"raw-body": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"rate-limiter-flexible": "2.4.1",
|
||||||
"rotating-file-stream": "^3.0.4",
|
"rotating-file-stream": "^3.0.4",
|
||||||
"swagger-ui-express": "4.3.0",
|
"swagger-ui-express": "4.3.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import Client from '../model/Client'
|
|||||||
import {
|
import {
|
||||||
getWebBuildFolder,
|
getWebBuildFolder,
|
||||||
generateAuthCode,
|
generateAuthCode,
|
||||||
|
getRateLimiters,
|
||||||
AuthProviderType,
|
AuthProviderType,
|
||||||
LDAPClient
|
LDAPClient,
|
||||||
|
secondsToHms
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
import { AuthController } from './auth'
|
import { AuthController } from './auth'
|
||||||
@@ -81,19 +83,98 @@ const login = async (
|
|||||||
req: express.Request,
|
req: express.Request,
|
||||||
{ username, password }: LoginPayload
|
{ username, password }: LoginPayload
|
||||||
) => {
|
) => {
|
||||||
|
// code for preventing brute force attack
|
||||||
|
|
||||||
|
const {
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
const { limiterSlowBruteByIP, limiterConsecutiveFailsByUsernameAndIP } =
|
||||||
|
getRateLimiters()
|
||||||
|
|
||||||
|
const ipAddr = req.ip
|
||||||
|
const usernameIPkey = getUsernameIPkey(username, ipAddr)
|
||||||
|
|
||||||
|
const [resSlowByIP, resUsernameAndIP] = await Promise.all([
|
||||||
|
limiterSlowBruteByIP.get(ipAddr),
|
||||||
|
limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
|
||||||
|
])
|
||||||
|
|
||||||
|
let retrySecs = 0
|
||||||
|
|
||||||
|
// Check if IP or Username + IP is already blocked
|
||||||
|
if (
|
||||||
|
resSlowByIP !== null &&
|
||||||
|
resSlowByIP.consumedPoints >= Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY)
|
||||||
|
) {
|
||||||
|
retrySecs = Math.round(resSlowByIP.msBeforeNext / 1000) || 1
|
||||||
|
} else if (
|
||||||
|
resUsernameAndIP !== null &&
|
||||||
|
resUsernameAndIP.consumedPoints >=
|
||||||
|
Number(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP)
|
||||||
|
) {
|
||||||
|
retrySecs = Math.round(resUsernameAndIP.msBeforeNext / 1000) || 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retrySecs > 0) {
|
||||||
|
throw {
|
||||||
|
code: 429,
|
||||||
|
message: `Too Many Requests! Retry after ${secondsToHms(retrySecs)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate User
|
// Authenticate User
|
||||||
const user = await User.findOne({ username })
|
const user = await User.findOne({ username })
|
||||||
if (!user) throw new Error('Username is not found.')
|
|
||||||
|
|
||||||
if (
|
let validPass = false
|
||||||
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
|
|
||||||
user.authProvider === AuthProviderType.LDAP
|
if (user) {
|
||||||
) {
|
if (
|
||||||
const ldapClient = await LDAPClient.init()
|
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
|
||||||
await ldapClient.verifyUser(username, password)
|
user.authProvider === AuthProviderType.LDAP
|
||||||
} else {
|
) {
|
||||||
const validPass = user.comparePassword(password)
|
const ldapClient = await LDAPClient.init()
|
||||||
if (!validPass) throw new Error('Invalid password.')
|
validPass = await ldapClient
|
||||||
|
.verifyUser(username, password)
|
||||||
|
.catch(() => false)
|
||||||
|
} else {
|
||||||
|
validPass = user.comparePassword(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume 1 point from limiters on wrong attempt and block if limits reached
|
||||||
|
if (!validPass) {
|
||||||
|
try {
|
||||||
|
const promises = [limiterSlowBruteByIP.consume(ipAddr)]
|
||||||
|
if (user) {
|
||||||
|
// Count failed attempts by Username + IP only for registered users
|
||||||
|
promises.push(
|
||||||
|
limiterConsecutiveFailsByUsernameAndIP.consume(usernameIPkey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
} catch (rlRejected: any) {
|
||||||
|
if (rlRejected instanceof Error) {
|
||||||
|
throw rlRejected
|
||||||
|
} else {
|
||||||
|
retrySecs = Math.round(rlRejected.msBeforeNext / 1000) || 1
|
||||||
|
|
||||||
|
throw {
|
||||||
|
code: 429,
|
||||||
|
message: `Too Many Requests! Retry after ${secondsToHms(retrySecs)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) throw { code: 401, message: 'Username is not found.' }
|
||||||
|
if (!validPass) throw { code: 401, message: 'Invalid Password.' }
|
||||||
|
|
||||||
|
if (resUsernameAndIP !== null && resUsernameAndIP.consumedPoints > 0) {
|
||||||
|
// Reset on successful authorization
|
||||||
|
await limiterConsecutiveFailsByUsernameAndIP.delete(usernameIPkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.session.loggedIn = true
|
req.session.loggedIn = true
|
||||||
@@ -144,6 +225,8 @@ const authorize = async (
|
|||||||
return { code }
|
return { code }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUsernameIPkey = (username: string, ip: string) => `${username}_${ip}`
|
||||||
|
|
||||||
interface LoginPayload {
|
interface LoginPayload {
|
||||||
/**
|
/**
|
||||||
* Username for user
|
* Username for user
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe('web', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('SASLogon/login', () => {
|
describe.only('SASLogon/login', () => {
|
||||||
let csrfToken: string
|
let csrfToken: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -63,6 +63,7 @@ describe('web', () => {
|
|||||||
it('should respond with successful login', async () => {
|
it('should respond with successful login', async () => {
|
||||||
await userController.createUser(user)
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
process.dbInstance = con
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASLogon/login')
|
.post('/SASLogon/login')
|
||||||
.set('x-xsrf-token', csrfToken)
|
.set('x-xsrf-token', csrfToken)
|
||||||
@@ -82,6 +83,72 @@ describe('web', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with too many requests when attempting with invalid password for a same user 10 times', async () => {
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
process.dbInstance = con
|
||||||
|
|
||||||
|
const promises: request.Test[] = []
|
||||||
|
|
||||||
|
Array(10)
|
||||||
|
.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)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.only('should respond with too many requests when attempting with invalid credentials for different users but with same ip 100 times', async () => {
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
process.dbInstance = con
|
||||||
|
|
||||||
|
const promises: request.Test[] = []
|
||||||
|
|
||||||
|
Array(100)
|
||||||
|
.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)
|
||||||
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if CSRF Token is not present', async () => {
|
it('should respond with Bad Request if CSRF Token is not present', async () => {
|
||||||
await userController.createUser(user)
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
|
|||||||
const response = await controller.login(req, body)
|
const response = await controller.login(req, body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
if (err instanceof Error) {
|
||||||
|
res.status(500).send(err.toString())
|
||||||
|
} else {
|
||||||
|
res.status(err.code).send(err.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
1
api/src/types/system/process.d.ts
vendored
1
api/src/types/system/process.d.ts
vendored
@@ -14,5 +14,6 @@ declare namespace NodeJS {
|
|||||||
logger: import('@sasjs/utils/logger').Logger
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
runTimes: import('../../utils').RunTimeType[]
|
runTimes: import('../../utils').RunTimeType[]
|
||||||
secrets: import('../../model/Configuration').ConfigurationType
|
secrets: import('../../model/Configuration').ConfigurationType
|
||||||
|
dbInstance: import('mongoose').Mongoose
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { seedDB } from './seedDB'
|
|||||||
|
|
||||||
export const connectDB = async () => {
|
export const connectDB = async () => {
|
||||||
try {
|
try {
|
||||||
await mongoose.connect(process.env.DB_CONNECT as string)
|
process.dbInstance = await mongoose.connect(
|
||||||
|
process.env.DB_CONNECT as string
|
||||||
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error('Unable to connect to DB!')
|
throw new Error('Unable to connect to DB!')
|
||||||
}
|
}
|
||||||
|
|||||||
26
api/src/utils/getRateLimiters.ts
Normal file
26
api/src/utils/getRateLimiters.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { RateLimiterMongo } from 'rate-limiter-flexible'
|
||||||
|
|
||||||
|
export const getRateLimiters = () => {
|
||||||
|
const {
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
const limiterSlowBruteByIP = new RateLimiterMongo({
|
||||||
|
storeClient: process.dbInstance.connection,
|
||||||
|
keyPrefix: 'login_fail_ip_per_day',
|
||||||
|
points: Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY),
|
||||||
|
duration: 60 * 60 * 24,
|
||||||
|
blockDuration: 60 * 60 * 24 // Block for 1 day
|
||||||
|
})
|
||||||
|
|
||||||
|
const limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMongo({
|
||||||
|
storeClient: process.dbInstance.connection,
|
||||||
|
keyPrefix: 'login_fail_consecutive_username_and_ip',
|
||||||
|
points: Number(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP),
|
||||||
|
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
|
||||||
|
blockDuration: 60 * 60 // Block for 1 hour
|
||||||
|
})
|
||||||
|
|
||||||
|
return { limiterSlowBruteByIP, limiterConsecutiveFailsByUsernameAndIP }
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ export * from './getAuthorizedRoutes'
|
|||||||
export * from './getCertificates'
|
export * from './getCertificates'
|
||||||
export * from './getDesktopFields'
|
export * from './getDesktopFields'
|
||||||
export * from './getPreProgramVariables'
|
export * from './getPreProgramVariables'
|
||||||
|
export * from './getRateLimiters'
|
||||||
export * from './getRunTimeAndFilePath'
|
export * from './getRunTimeAndFilePath'
|
||||||
export * from './getServerUrl'
|
export * from './getServerUrl'
|
||||||
export * from './getTokensFromDB'
|
export * from './getTokensFromDB'
|
||||||
@@ -20,10 +21,10 @@ export * from './instantiateLogger'
|
|||||||
export * from './isDebugOn'
|
export * from './isDebugOn'
|
||||||
export * from './isPublicRoute'
|
export * from './isPublicRoute'
|
||||||
export * from './ldapClient'
|
export * from './ldapClient'
|
||||||
export * from './zipped'
|
|
||||||
export * from './parseLogToArray'
|
export * from './parseLogToArray'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
export * from './saveTokensInDB'
|
export * from './saveTokensInDB'
|
||||||
|
export * from './secondsToHms'
|
||||||
export * from './seedDB'
|
export * from './seedDB'
|
||||||
export * from './setProcessVariables'
|
export * from './setProcessVariables'
|
||||||
export * from './setupFolders'
|
export * from './setupFolders'
|
||||||
@@ -32,3 +33,4 @@ export * from './upload'
|
|||||||
export * from './validation'
|
export * from './validation'
|
||||||
export * from './verifyEnvVariables'
|
export * from './verifyEnvVariables'
|
||||||
export * from './verifyTokenInDB'
|
export * from './verifyTokenInDB'
|
||||||
|
export * from './zipped'
|
||||||
|
|||||||
10
api/src/utils/secondsToHms.ts
Normal file
10
api/src/utils/secondsToHms.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export const secondsToHms = (seconds: number) => {
|
||||||
|
const h = Math.floor(seconds / 3600)
|
||||||
|
const m = Math.floor((seconds % 3600) / 60)
|
||||||
|
const s = Math.floor((seconds % 3600) % 60)
|
||||||
|
|
||||||
|
const hDisplay = h > 0 ? h + (h == 1 ? ' hour, ' : ' hours, ') : ''
|
||||||
|
const mDisplay = m > 0 ? m + (m == 1 ? ' minute, ' : ' minutes, ') : ''
|
||||||
|
const sDisplay = s > 0 ? s + (s == 1 ? ' second' : ' seconds') : ''
|
||||||
|
return hDisplay + mDisplay + sDisplay
|
||||||
|
}
|
||||||
@@ -77,6 +77,8 @@ export const verifyEnvVariables = (): ReturnCode => {
|
|||||||
|
|
||||||
errors.push(...verifyDbType())
|
errors.push(...verifyDbType())
|
||||||
|
|
||||||
|
errors.push(...verifyRateLimiter())
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
process.logger?.error(
|
process.logger?.error(
|
||||||
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
||||||
@@ -367,6 +369,50 @@ const verifyDbType = () => {
|
|||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verifyRateLimiter = () => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const {
|
||||||
|
MODE,
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
} = process.env
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
if (MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) {
|
||||||
|
if (
|
||||||
|
!isNumeric(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) ||
|
||||||
|
Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) < 1
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`- Invalid value for 'MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY' - Only positive number is acceptable`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY =
|
||||||
|
DEFAULTS.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) {
|
||||||
|
if (
|
||||||
|
!isNumeric(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) ||
|
||||||
|
Number(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) < 1
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`- Invalid value for 'MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP' - Only positive number is acceptable`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP =
|
||||||
|
DEFAULTS.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumeric = (val: string): boolean => {
|
||||||
|
return !isNaN(Number(val))
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
MODE: ModeType.Desktop,
|
MODE: ModeType.Desktop,
|
||||||
PROTOCOL: ProtocolType.HTTP,
|
PROTOCOL: ProtocolType.HTTP,
|
||||||
@@ -374,5 +420,7 @@ const DEFAULTS = {
|
|||||||
HELMET_COEP: HelmetCoepType.TRUE,
|
HELMET_COEP: HelmetCoepType.TRUE,
|
||||||
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
||||||
RUN_TIMES: RunTimeType.SAS,
|
RUN_TIMES: RunTimeType.SAS,
|
||||||
DB_TYPE: DatabaseType.MONGO
|
DB_TYPE: DatabaseType.MONGO,
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY: '100',
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP: '10'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user