mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-02-07 13:59:01 +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
|
https://github.com/animir/node-rate-limiter-flexible/wiki/Memory
|
||||||
*/
|
*/
|
||||||
rateLimiters: [
|
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
|
// 6 requests in 30 seconds
|
||||||
routes: [
|
routes: [
|
||||||
|
@ -36,12 +36,12 @@ const usersPerPage = config.dashboard
|
|||||||
: 25
|
: 25
|
||||||
|
|
||||||
// ip is an optional parameter, which if set will be rate limited
|
// 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) => {
|
self.assertUser = async (token, fields, ip) => {
|
||||||
if (ip) {
|
if (ip) {
|
||||||
const rateLimiterRes = await tokens.invalidTokenRateLimiter.get(ip)
|
const rateLimiterRes = await tokens.authFailuresRateLimiter.get(ip)
|
||||||
if (rateLimiterRes && rateLimiterRes.remainingPoints <= 0) {
|
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 {
|
} else {
|
||||||
if (ip) {
|
if (ip) {
|
||||||
// Rate limit attempts with invalid token
|
// 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 })
|
throw new ClientError('Invalid token.', { statusCode: 403, code: 10001 })
|
||||||
}
|
}
|
||||||
@ -122,11 +122,18 @@ self.verify = async (req, res) => {
|
|||||||
throw new ClientError('No password provided.')
|
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')
|
const user = await utils.db.table('users')
|
||||||
.where('username', username)
|
.where('username', username)
|
||||||
.first()
|
.first()
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
await tokens.authFailuresRateLimiter.consume(req.ip, 1)
|
||||||
throw new ClientError('Wrong credentials.', { statusCode: 403 })
|
throw new ClientError('Wrong credentials.', { statusCode: 403 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +143,7 @@ self.verify = async (req, res) => {
|
|||||||
|
|
||||||
const result = await bcrypt.compare(password, user.password)
|
const result = await bcrypt.compare(password, user.password)
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
|
await tokens.authFailuresRateLimiter.consume(req.ip, 1)
|
||||||
throw new ClientError('Wrong credentials.', { statusCode: 403 })
|
throw new ClientError('Wrong credentials.', { statusCode: 403 })
|
||||||
} else {
|
} else {
|
||||||
return res.json({ success: true, token: user.token })
|
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.`)
|
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')
|
const user = await utils.db.table('users')
|
||||||
.where('username', username)
|
.where('username', username)
|
||||||
.first()
|
.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)
|
const hash = await bcrypt.hash(password, saltRounds)
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@ const self = {
|
|||||||
|
|
||||||
onHold: new Set(), // temporarily held random tokens
|
onHold: new Set(), // temporarily held random tokens
|
||||||
|
|
||||||
// Maximum of 6 token auth failures in 10 minutes
|
// Maximum of 6 auth failures in 10 minutes
|
||||||
invalidTokenRateLimiter: new RateLimiterMemory({
|
authFailuresRateLimiter: new RateLimiterMemory({
|
||||||
points: 6,
|
points: 6,
|
||||||
duration: 10 * 60
|
duration: 10 * 60
|
||||||
})
|
})
|
||||||
@ -78,9 +78,9 @@ self.verify = async (req, res) => {
|
|||||||
throw new ClientError('No token provided.', { statusCode: 403 })
|
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) {
|
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')
|
const user = await utils.db.table('users')
|
||||||
@ -90,7 +90,7 @@ self.verify = async (req, res) => {
|
|||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// Rate limit attempts with invalid token
|
// 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 })
|
throw new ClientError('Invalid token.', { statusCode: 403, code: 10001 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user