mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-18 17:21:33 +00:00
feat: rate limit token auth failures
hard-coded to max 6 failures in 10 minutes
This commit is contained in:
parent
abe27b746c
commit
a406f85215
@ -35,7 +35,16 @@ const usersPerPage = config.dashboard
|
|||||||
? Math.max(Math.min(config.dashboard.usersPerPage || 0, 100), 1)
|
? Math.max(Math.min(config.dashboard.usersPerPage || 0, 100), 1)
|
||||||
: 25
|
: 25
|
||||||
|
|
||||||
self.assertUser = async (token, fields) => {
|
// ip is an optional parameter, which if set will be rate limited
|
||||||
|
// using tokens.invalidTokenRateLimiter pool
|
||||||
|
self.assertUser = async (token, fields, ip) => {
|
||||||
|
if (ip) {
|
||||||
|
const rateLimiterRes = await tokens.invalidTokenRateLimiter.get(ip)
|
||||||
|
if (rateLimiterRes && rateLimiterRes.remainingPoints <= 0) {
|
||||||
|
throw new ClientError('Too many requests with invalid token. Try again in a while.', { statusCode: 429 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Default fields/columns to fetch from database
|
// Default fields/columns to fetch from database
|
||||||
const _fields = ['id', 'username', 'enabled', 'timestamp', 'permission', 'registration']
|
const _fields = ['id', 'username', 'enabled', 'timestamp', 'permission', 'registration']
|
||||||
|
|
||||||
@ -57,32 +66,44 @@ self.assertUser = async (token, fields) => {
|
|||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
} else {
|
} else {
|
||||||
throw new ClientError('Invalid token.', { statusCode: 403 })
|
if (ip) {
|
||||||
|
// Rate limit attempts with invalid token
|
||||||
|
await tokens.invalidTokenRateLimiter.consume(ip, 1)
|
||||||
|
}
|
||||||
|
throw new ClientError('Invalid token.', { statusCode: 403, code: 10001 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// _ is next() if this was a synchronous middleware function
|
self.requireUser = (req, res, next, fields) => {
|
||||||
self.requireUser = async (req, res, _, fields) => {
|
|
||||||
// Throws when token is missing, thus use only for users-only routes
|
// Throws when token is missing, thus use only for users-only routes
|
||||||
const token = req.headers.token
|
const token = req.headers.token
|
||||||
if (token === undefined) {
|
if (token === undefined) {
|
||||||
throw new ClientError('No token provided.', { statusCode: 403 })
|
return next(new ClientError('No token provided.', { statusCode: 403 }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user data to Request.locals.user
|
self.assertUser(token, fields, req.ip)
|
||||||
req.locals.user = await self.assertUser(token, fields)
|
.then(user => {
|
||||||
|
// Add user data to Request.locals.user
|
||||||
|
req.locals.user = user
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
.catch(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
// _ is next() if this was a synchronous middleware function
|
self.optionalUser = (req, res, next, fields) => {
|
||||||
self.optionalUser = async (req, res, _, fields) => {
|
|
||||||
// Throws when token if missing only when private is set to true in config,
|
// Throws when token if missing only when private is set to true in config,
|
||||||
// thus use for routes that can handle no auth requests
|
// thus use for routes that can handle no auth requests
|
||||||
const token = req.headers.token
|
const token = req.headers.token
|
||||||
if (token) {
|
if (token) {
|
||||||
// Add user data to Request.locals.user
|
self.assertUser(token, fields, req.ip)
|
||||||
req.locals.user = await self.assertUser(token, fields)
|
.then(user => {
|
||||||
|
// Add user data to Request.locals.user
|
||||||
|
req.locals.user = user
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
.catch(next)
|
||||||
} else if (config.private === true) {
|
} else if (config.private === true) {
|
||||||
throw new ClientError('No token provided.', { statusCode: 403 })
|
return next(new ClientError('No token provided.', { statusCode: 403 }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const randomstring = require('randomstring')
|
const randomstring = require('randomstring')
|
||||||
|
const { RateLimiterMemory } = require('rate-limiter-flexible')
|
||||||
const perms = require('./permissionController')
|
const perms = require('./permissionController')
|
||||||
const utils = require('./utilsController')
|
const utils = require('./utilsController')
|
||||||
const ClientError = require('./utils/ClientError')
|
const ClientError = require('./utils/ClientError')
|
||||||
@ -9,7 +10,13 @@ const self = {
|
|||||||
tokenLength: 64,
|
tokenLength: 64,
|
||||||
tokenMaxTries: 3,
|
tokenMaxTries: 3,
|
||||||
|
|
||||||
onHold: new Set() // temporarily held random tokens
|
onHold: new Set(), // temporarily held random tokens
|
||||||
|
|
||||||
|
// Maximum of 6 token auth failures in 10 minutes
|
||||||
|
invalidTokenRateLimiter: new RateLimiterMemory({
|
||||||
|
points: 6,
|
||||||
|
duration: 10 * 60
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
self.getUniqueToken = async res => {
|
self.getUniqueToken = async res => {
|
||||||
@ -71,12 +78,19 @@ 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)
|
||||||
|
if (rateLimiterRes && rateLimiterRes.remainingPoints <= 0) {
|
||||||
|
throw new ClientError('Too many requests with invalid token. Try again in a while.', { statusCode: 429 })
|
||||||
|
}
|
||||||
|
|
||||||
const user = await utils.db.table('users')
|
const user = await utils.db.table('users')
|
||||||
.where('token', token)
|
.where('token', token)
|
||||||
.select('username', 'permission')
|
.select('username', 'permission')
|
||||||
.first()
|
.first()
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
// Rate limit attempts with invalid token
|
||||||
|
await self.invalidTokenRateLimiter.consume(req.ip, 1)
|
||||||
throw new ClientError('Invalid token.', { statusCode: 403, code: 10001 })
|
throw new ClientError('Invalid token.', { statusCode: 403, code: 10001 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,9 +77,9 @@ routes.post('/albums/rename', [auth.requireUser, utils.assertJSON], albums.renam
|
|||||||
/** ./controllers/tokenController.js **/
|
/** ./controllers/tokenController.js **/
|
||||||
|
|
||||||
routes.get('/tokens', auth.requireUser, tokens.list)
|
routes.get('/tokens', auth.requireUser, tokens.list)
|
||||||
routes.post('/tokens/change', async (req, res) => {
|
routes.post('/tokens/change', (req, res, next) => {
|
||||||
// Include user's "token" field into database query
|
// Include user's "token" field into database query
|
||||||
return auth.requireUser(req, res, null, 'token')
|
auth.requireUser(req, res, next, 'token')
|
||||||
}, tokens.change)
|
}, tokens.change)
|
||||||
routes.post('/tokens/verify', utils.assertJSON, tokens.verify)
|
routes.post('/tokens/verify', utils.assertJSON, tokens.verify)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user