Login rate limiting

main
Ashelyn Dawn 3 years ago
parent 2fd23f4615
commit 1289b56b2b

@ -2,6 +2,8 @@ const router = require('express-promise-router')()
const parseJSON = require('body-parser').json()
const db = require('../db')
const {loginRateLimit} = require('./middleware/rateLimits')
const validate = require('./middleware/validators')
const loginValidation = [
@ -10,14 +12,14 @@ const loginValidation = [
validate.handleApiError
]
router.post('/', parseJSON, loginValidation, async (req, res) => {
router.post('/', parseJSON, loginValidation, loginRateLimit, async (req, res) => {
const user = await db.user.login(
req.body.email,
req.body.password
)
if(!user){
return res.status(422).json({errors: [{
return res.status(403).json({errors: [{
param: 'email',
msg: 'Invalid login'
},{
@ -26,6 +28,8 @@ router.post('/', parseJSON, loginValidation, async (req, res) => {
}]})
}
await loginRateLimit.reset(req.body.email);
const cart = req.sessionObj?.cart
const session = await db.session.create(req, user)

@ -0,0 +1,59 @@
const {RateLimiterPostgres, RateLimiterUnion} = require('rate-limiter-flexible');
const pg = require('../../db/pg')
const globalOptions = {
storeClient: pg,
tableName: 'sos.rate_limits',
tableCreated: true
}
function createRateLimit(maxTimes, duration, prefix) {
return new RateLimiterPostgres({
...globalOptions,
points: maxTimes,
inmemoryBlockOnConsumed: maxTimes,
keyPrefix: prefix,
duration
})
}
module.exports = {};
const loginRateLimits = [
// Once per ten seconds
createRateLimit(1, 10, 'logon1'),
// Ten tries in five minutes
createRateLimit(10, 5 * 60, 'logon2'),
// One hundred in a day
createRateLimit(100, 24 * 60 * 60, 'logon3')
]
const loginRateLimitPool = new RateLimiterUnion(...loginRateLimits)
module.exports.loginRateLimit = async (req, res) => {
try {
await loginRateLimitPool.consume(req.body.email)
return 'next';
} catch (limits) {
const wait = Math.max(...Object.values(limits).map(limit => limit?.msBeforeNext || 1));
res.status(429)
res.set({
'Retry-After': Math.ceil(wait / 1000)
})
res.json({errors: [{
param: 'email',
msg: `Too many password attempts, please wait ${Math.ceil(wait / 1000)} seconds`
},{
param: 'password',
msg: ' '
}]})
}
}
module.exports.loginRateLimit.reset = email => Promise.all(
loginRateLimits.map(rateLimit =>
rateLimit.delete(email)
)
)

@ -113,7 +113,7 @@ router.put('/current/password', parseJSON, changePasswordValidation, ensureUser,
)
if(!user){
return res.status(422).json({errors: [{
return res.status(403).json({errors: [{
param: 'oldPassword',
msg: 'Incorrect password'
}]})

@ -133,7 +133,8 @@ export const FormController = function FormController({children, className, url,
const {data: response} = await axios({ method, url, data })
return afterSubmit(response)
} catch (err) {
if(!err.response || err.response.status !== 422) throw err;
const status = err?.response?.status;
if(![400, 401, 403, 422, 429].includes(status)) throw err;
return errorDispatch({
type: 'set_errors',

@ -291,4 +291,10 @@ create table sos."config" (
config_shipping_from uuid references sos."address" (address_uuid)
);
create table sos."rate_limits" (
key varchar(255) primary key,
points integer not null default 0,
expire bigint
);
insert into sos."config" default values;

5
package-lock.json generated

@ -5502,6 +5502,11 @@
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"rate-limiter-flexible": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.1.tgz",
"integrity": "sha512-rxCP6kDDdn0cZmVqVlF06yLU+mG3TuwaHV/fUIw3OQyYhza7pzVBtdMhUmfXbBzMS+O464XP+x33pfTDGRGYVA=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",

@ -36,6 +36,7 @@
"next": "^10.0.3",
"next-images": "^1.6.2",
"pg": "^8.5.1",
"rate-limiter-flexible": "^2.2.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-infinite-calendar": "^2.3.1",

Loading…
Cancel
Save