feat: rate limit token auth failures

hard-coded to max 6 failures in 10 minutes
This commit is contained in:
Bobby Wibowo 2022-08-04 23:09:14 +07:00
parent abe27b746c
commit a406f85215
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
3 changed files with 50 additions and 15 deletions

View File

@ -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 }))
} }
} }

View File

@ -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 })
} }

View File

@ -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)