mirror of
https://github.com/sasjs/server.git
synced 2026-01-06 14:10:06 +00:00
chore: move brute force protection logic to middleware and a singleton class
This commit is contained in:
@@ -8,7 +8,7 @@ import Client from '../model/Client'
|
||||
import {
|
||||
getWebBuildFolder,
|
||||
generateAuthCode,
|
||||
getRateLimiters,
|
||||
RateLimiter,
|
||||
AuthProviderType,
|
||||
LDAPClient,
|
||||
secondsToHms
|
||||
@@ -83,47 +83,6 @@ const login = async (
|
||||
req: express.Request,
|
||||
{ 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
|
||||
const user = await User.findOne({ username })
|
||||
|
||||
@@ -143,28 +102,16 @@ const login = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Consume 1 point from limiters on wrong attempt and block if limits reached
|
||||
// code to prevent brute force attack
|
||||
|
||||
const rateLimiter = RateLimiter.getInstance()
|
||||
|
||||
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)}`
|
||||
}
|
||||
const retrySecs = await rateLimiter.consume(req.ip, user?.username)
|
||||
if (retrySecs > 0) {
|
||||
throw {
|
||||
code: 429,
|
||||
message: `Too Many Requests! Retry after ${secondsToHms(retrySecs)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,10 +119,8 @@ const login = async (
|
||||
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)
|
||||
}
|
||||
// Reset on successful authorization
|
||||
rateLimiter.resetOnSuccess(req.ip, user.username)
|
||||
|
||||
req.session.loggedIn = true
|
||||
req.session.user = {
|
||||
|
||||
Reference in New Issue
Block a user