1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-07 06:30:06 +00:00

chore: move brute force protection logic to middleware and a singleton class

This commit is contained in:
2023-03-29 15:33:32 +05:00
parent a82cabb001
commit 89048ce943
12 changed files with 190 additions and 121 deletions

View File

@@ -3,9 +3,7 @@ import { seedDB } from './seedDB'
export const connectDB = async () => {
try {
process.dbInstance = await mongoose.connect(
process.env.DB_CONNECT as string
)
await mongoose.connect(process.env.DB_CONNECT as string)
} catch (err) {
throw new Error('Unable to connect to DB!')
}

View File

@@ -1,26 +0,0 @@
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 }
}

View File

@@ -0,0 +1,2 @@
export const getUsernameIPKey = (username: string, ip: string) =>
`${username}_${ip}`

View File

@@ -13,7 +13,6 @@ export * from './getAuthorizedRoutes'
export * from './getCertificates'
export * from './getDesktopFields'
export * from './getPreProgramVariables'
export * from './getRateLimiters'
export * from './getRunTimeAndFilePath'
export * from './getServerUrl'
export * from './getTokensFromDB'
@@ -22,6 +21,7 @@ export * from './isDebugOn'
export * from './isPublicRoute'
export * from './ldapClient'
export * from './parseLogToArray'
export * from './rateLimiter'
export * from './removeTokensInDB'
export * from './saveTokensInDB'
export * from './secondsToHms'

View File

@@ -0,0 +1,117 @@
import mongoose from 'mongoose'
import { RateLimiterMongo } from 'rate-limiter-flexible'
export class RateLimiter {
private static instance: RateLimiter
private limiterSlowBruteByIP: RateLimiterMongo
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMongo
private maxWrongAttemptsByIpPerDay: number
private maxConsecutiveFailsByUsernameAndIp: number
private constructor() {
const {
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
} = process.env
this.maxWrongAttemptsByIpPerDay = Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY)
this.maxConsecutiveFailsByUsernameAndIp = Number(
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
)
this.limiterSlowBruteByIP = new RateLimiterMongo({
storeClient: mongoose.connection,
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,
keyPrefix: 'login_fail_consecutive_username_and_ip',
points: this.maxConsecutiveFailsByUsernameAndIp,
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
blockDuration: 60 * 60 // Block for 1 hour
})
}
public static getInstance() {
if (!RateLimiter.instance) {
RateLimiter.instance = new RateLimiter()
}
return RateLimiter.instance
}
private getUsernameIPKey(ip: string, username: string) {
return `${username}_${ip}`
}
/**
* This method checks for brute force attack
* If attack is detected then returns the number of seconds after which user can make another request
* Else returns 0
*/
public async check(ip: string, username: string) {
const usernameIPkey = this.getUsernameIPKey(ip, username)
const [resSlowByIP, resUsernameAndIP] = await Promise.all([
this.limiterSlowBruteByIP.get(ip),
this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
])
// Check if IP or Username + IP is already blocked
if (
resSlowByIP !== null &&
resSlowByIP.consumedPoints >= this.maxWrongAttemptsByIpPerDay
) {
return Math.ceil(resSlowByIP.msBeforeNext / 1000)
} else if (
resUsernameAndIP !== null &&
resUsernameAndIP.consumedPoints >= this.maxConsecutiveFailsByUsernameAndIp
) {
return Math.ceil(resUsernameAndIP.msBeforeNext / 1000)
}
return 0
}
/**
* Consume 1 point from limiters on wrong attempt and block if limits reached
* If limit is reached, return the number of seconds after which user can make another request
* Else return 0
*/
public async consume(ip: string, username?: string) {
try {
const promises = [this.limiterSlowBruteByIP.consume(ip)]
if (username) {
const usernameIPkey = this.getUsernameIPKey(ip, username)
// Count failed attempts by Username + IP only for registered users
promises.push(
this.limiterConsecutiveFailsByUsernameAndIP.consume(usernameIPkey)
)
}
await Promise.all(promises)
} catch (rlRejected: any) {
if (rlRejected instanceof Error) {
throw rlRejected
} else {
return Math.ceil(rlRejected.msBeforeNext / 1000)
}
}
return 0
}
public async resetOnSuccess(ip: string, username: string) {
const usernameIPkey = this.getUsernameIPKey(ip, username)
const resUsernameAndIP =
await this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
if (resUsernameAndIP !== null && resUsernameAndIP.consumedPoints > 0) {
await this.limiterConsecutiveFailsByUsernameAndIP.delete(usernameIPkey)
}
}
}