diff --git a/package.json b/package.json index 7faeab95f..fdb2acb63 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "mongoose": "6.2.2", "mongoose-autopopulate": "0.16.0", "mongoose-lean-virtuals": "0.9.0", - "nconf": "0.11.3", + "nconf": "0.12.0", "netmask": "2.0.2", "node-cache": "5.1.2", "node-sass": "7.0.1", @@ -87,6 +87,7 @@ "piexifjs": "1.0.6", "pm2": "5.1.2", "prop-types": "15.8.1", + "rate-limiter-flexible": "2.3.7", "react": "17.0.2", "react-colorful": "5.5.1", "react-dom": "17.0.2", diff --git a/src/controllers/main.js b/src/controllers/main.js index a2ea5a45a..979bd3ad6 100644 --- a/src/controllers/main.js +++ b/src/controllers/main.js @@ -19,6 +19,14 @@ const passport = require('passport') const winston = require('winston') const pkg = require('../../package') const xss = require('xss') +const RateLimiterMemory = require('rate-limiter-flexible').RateLimiterMemory + +const limiterSlowBruteByIP = new RateLimiterMemory({ + keyPrefix: 'login_fail_ip_per_day', + points: 15, + duration: 60 * 60 * 24, + blockDuration: 60 * 60 +}) const mainController = {} @@ -97,34 +105,62 @@ mainController.dashboard = function (req, res) { return res.render('dashboard', content) } -mainController.loginPost = function (req, res, next) { - passport.authenticate('local', function (err, user) { - if (err) { - winston.error(err) - return next(err) - } - if (!user) return res.redirect('/') - - let redirectUrl = '/dashboard' +mainController.loginPost = async function (req, res, next) { + const ipAddress = req.ip + const [resEmailAndIP] = await Promise.all([limiterSlowBruteByIP.get(ipAddress)]) - if (req.session.redirectUrl) { - redirectUrl = req.session.redirectUrl - req.session.redirectUrl = null - } - - if (req.user.role === 'user') { - redirectUrl = '/tickets' - } + let retrySecs = 0 + if (resEmailAndIP !== null && resEmailAndIP.consumedPoints > 2) { + retrySecs = Math.round(resEmailAndIP.msBeforeNext / 1000) || 1 + } - req.logIn(user, function (err) { + if (retrySecs > 0) { + res.set('Retry-After', retrySecs.toString()) + // res.status(429).send(`Too many requests. Retry after ${retrySecs} seconds.`) + res.status(429).render('429', { timeout: retrySecs.toString(), layout: false }) + } else { + passport.authenticate('local', async function (err, user) { if (err) { - winston.debug(err) + winston.error(err) return next(err) } + if (!user) { + try { + await limiterSlowBruteByIP.consume(ipAddress) + return res.redirect('/') + } catch (rlRejected) { + if (rlRejected instanceof Error) throw rlRejected + else { + const timeout = String(Math.round(rlRejected.msBeforeNext / 1000)) || 1 + res.set('Retry-After', timeout) + res.status(429).render('429', { timeout, layout: false }) + } + } + } - return res.redirect(redirectUrl) - }) - })(req, res, next) + if (user) { + let redirectUrl = '/dashboard' + + if (req.session.redirectUrl) { + redirectUrl = req.session.redirectUrl + req.session.redirectUrl = null + } + + if (req.user.role === 'user') { + redirectUrl = '/tickets' + } + + req.logIn(user, function (err) { + if (err) { + winston.debug(err) + return next(err) + } + + return res.redirect(redirectUrl) + }) + } + })(req, res, next) + } } mainController.l2AuthPost = function (req, res, next) { diff --git a/src/passport/index.js b/src/passport/index.js index 0bc260cf3..90c799e5e 100644 --- a/src/passport/index.js +++ b/src/passport/index.js @@ -49,11 +49,13 @@ module.exports = function () { } if (!user || user.deleted) { - return done(null, false, req.flash('loginMessage', 'No User Found.')) + req.flash('loginMessage', '') + return done(null, false, req.flash('loginMessage', 'Invalid Username/Password')) } if (!User.validate(password, user.password)) { - return done(null, false, req.flash('loginMessage', 'Incorrect Password.')) + req.flash('loginMessage', '') + return done(null, false, req.flash('loginMessage', 'Invalid Username/Password')) } req.user = user diff --git a/src/routes/index.js b/src/routes/index.js index a647dcbde..842de7d61 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -455,6 +455,11 @@ function handleErrors (err, req, res) { const status = err.status || 500 res.status(err.status) + if (status === 429) { + res.render('429', { layout: false }) + return + } + if (status === 500) { res.render('500', { layout: false }) return diff --git a/src/views/429.hbs b/src/views/429.hbs new file mode 100644 index 000000000..0a6b838ac --- /dev/null +++ b/src/views/429.hbs @@ -0,0 +1,120 @@ + + +
+