mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-19 01:31:34 +00:00
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:
parent
a406f85215
commit
c6c485447f
@ -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: [
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 })
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user