feat: token failure rate limit on login/register

also removed default 2 reqs in 5s rate limiter for login/register routes
from sample config, as it's pretty much redundant now
This commit is contained in:
Bobby Wibowo 2022-08-04 23:34:58 +07:00
parent a406f85215
commit c6c485447f
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
3 changed files with 29 additions and 22 deletions

View File

@ -218,18 +218,6 @@ module.exports = {
https://github.com/animir/node-rate-limiter-flexible/wiki/Memory
*/
rateLimiters: [
{
// 2 requests in 5 seconds
routes: [
// If multiple routes, they will share the same points pool
'/api/login',
'/api/register'
],
options: {
points: 2,
duration: 5
}
},
{
// 6 requests in 30 seconds
routes: [

View File

@ -36,12 +36,12 @@ const usersPerPage = config.dashboard
: 25
// ip is an optional parameter, which if set will be rate limited
// using tokens.invalidTokenRateLimiter pool
// using tokens.authFailuresRateLimiter pool
self.assertUser = async (token, fields, ip) => {
if (ip) {
const rateLimiterRes = await tokens.invalidTokenRateLimiter.get(ip)
const rateLimiterRes = await tokens.authFailuresRateLimiter.get(ip)
if (rateLimiterRes && rateLimiterRes.remainingPoints <= 0) {
throw new ClientError('Too many requests with invalid token. Try again in a while.', { statusCode: 429 })
throw new ClientError('Too many auth failures. Try again in a while.', { statusCode: 429 })
}
}
@ -68,7 +68,7 @@ self.assertUser = async (token, fields, ip) => {
} else {
if (ip) {
// Rate limit attempts with invalid token
await tokens.invalidTokenRateLimiter.consume(ip, 1)
await tokens.authFailuresRateLimiter.consume(ip, 1)
}
throw new ClientError('Invalid token.', { statusCode: 403, code: 10001 })
}
@ -122,11 +122,18 @@ self.verify = async (req, res) => {
throw new ClientError('No password provided.')
}
// Use tokens.authFailuresRateLimiter pool for /api/login as well
const rateLimiterRes = await tokens.authFailuresRateLimiter.get(req.ip)
if (rateLimiterRes && rateLimiterRes.remainingPoints <= 0) {
throw new ClientError('Too many auth failures. Try again in a while.', { statusCode: 429 })
}
const user = await utils.db.table('users')
.where('username', username)
.first()
if (!user) {
await tokens.authFailuresRateLimiter.consume(req.ip, 1)
throw new ClientError('Wrong credentials.', { statusCode: 403 })
}
@ -136,6 +143,7 @@ self.verify = async (req, res) => {
const result = await bcrypt.compare(password, user.password)
if (result === false) {
await tokens.authFailuresRateLimiter.consume(req.ip, 1)
throw new ClientError('Wrong credentials.', { statusCode: 403 })
} else {
return res.json({ success: true, token: user.token })
@ -161,11 +169,22 @@ self.register = async (req, res) => {
throw new ClientError(`Password must have ${self.pass.min}-${self.pass.max} characters.`)
}
// Use tokens.authFailuresRateLimiter pool for /api/register as well
const rateLimiterRes = await tokens.authFailuresRateLimiter.get(req.ip)
if (rateLimiterRes && rateLimiterRes.remainingPoints <= 0) {
throw new ClientError('Too many auth failures. Try again in a while.', { statusCode: 429 })
}
const user = await utils.db.table('users')
.where('username', username)
.first()
if (user) throw new ClientError('Username already exists.')
if (user) {
// Also consume rate limit to protect this route
// from being brute-forced to find existing usernames
await tokens.authFailuresRateLimiter.consume(req.ip, 1)
throw new ClientError('Username already exists.')
}
const hash = await bcrypt.hash(password, saltRounds)

View File

@ -12,8 +12,8 @@ const self = {
onHold: new Set(), // temporarily held random tokens
// Maximum of 6 token auth failures in 10 minutes
invalidTokenRateLimiter: new RateLimiterMemory({
// Maximum of 6 auth failures in 10 minutes
authFailuresRateLimiter: new RateLimiterMemory({
points: 6,
duration: 10 * 60
})
@ -78,9 +78,9 @@ self.verify = async (req, res) => {
throw new ClientError('No token provided.', { statusCode: 403 })
}
const rateLimiterRes = await self.invalidTokenRateLimiter.get(req.ip)
const rateLimiterRes = await self.authFailuresRateLimiter.get(req.ip)
if (rateLimiterRes && rateLimiterRes.remainingPoints <= 0) {
throw new ClientError('Too many requests with invalid token. Try again in a while.', { statusCode: 429 })
throw new ClientError('Too many auth failures. Try again in a while.', { statusCode: 429 })
}
const user = await utils.db.table('users')
@ -90,7 +90,7 @@ self.verify = async (req, res) => {
if (!user) {
// Rate limit attempts with invalid token
await self.invalidTokenRateLimiter.consume(req.ip, 1)
await self.authFailuresRateLimiter.consume(req.ip, 1)
throw new ClientError('Invalid token.', { statusCode: 403, code: 10001 })
}