Skip to content

Commit

Permalink
fix(login): basic rate limit protection
Browse files Browse the repository at this point in the history
  • Loading branch information
polonel committed May 16, 2022
1 parent e836d04 commit 526cef3
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 36 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
80 changes: 58 additions & 22 deletions src/controllers/main.js
Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions src/passport/index.js
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/routes/index.js
Expand Up @@ -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
Expand Down
120 changes: 120 additions & 0 deletions src/views/429.hbs
@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>
<head>
<title>Trudesk</title>
<link href='http://fonts.googleapis.com/css?family=Roboto:200,400,300,600,700' rel='stylesheet' type='text/css'>
<style type="text/css">
html {
overflow-x: hidden;
}
body {
background: #eee;
text-align: center;
font-family: "Roboto", Arial, sans-serif;
}
.wrapper {
width: 400px;
height: 400px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
img {
margin-bottom: 55px;
}
label {
text-align: left
}
button {
font-family: "Roboto", Arial, sans-serif;
width: 340px;
height: 20px;
text-align: left;
padding: 15px 20px;
background: #384b5f;
border: 1px solid #1c2937;
font-size: 20px;
color: #fff;
margin-bottom: 15px;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
font-weight: 300;
}
button.button {
width: 380px;
height: auto;
text-align: center;
background: #e74b3b;
border: none;
text-transform: uppercase;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
}
button.button:hover {
background: #f15849;
cursor: pointer;
}
*:focus {
outline: 0;
}
a {
display: inline-block;
padding-right: 10px;
color: #727272;
font-size: 14px;
float: right;
text-decoration: none;
font-weight: 300;
}
a:hover {
color: #A5A5A5;
}
::-webkit-input-placeholder { /* WebKit browsers */
color: #444;
}
:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
color: #444;
opacity: 1;
}
::-moz-placeholder { /* Mozilla Firefox 19+ */
color: #444;
opacity: 1;
}
:-ms-input-placeholder { /* Internet Explorer 10+ */
color: #444;
}
h2,h4 {
text-align: center;
font-family: "Roboto", Arial, sans-serif;
font-size: 150px;
font-weight: 300;
margin: 0;
color: #666;
}
h4 {
font-size: 32px;
color: #666;
}
</style>
</head>
<body>
<a href="/" ><img src="/img/td_3@2x.png" alt="Trudesk Logo" style="display: block; position: absolute; top: 15px; left: 15px; height: 75px" /></a>
<div class="wrapper">
<h2>429</h2>
<h4>Too many requests. <br />Please wait. ({{timeout}})</h4>
</div>
</body>
</html>
22 changes: 11 additions & 11 deletions yarn.lock
Expand Up @@ -3144,16 +3144,11 @@ async@1.4.2:
resolved "https://registry.yarnpkg.com/async/-/async-1.4.2.tgz#6c9edcb11ced4f0dd2f2d40db0d49a109c088aab"
integrity sha1-bJ7csRztTw3S8tQNsNSaEJwIiqs=

async@3.2.3, async@^3.2.3:
async@3.2.3, async@^3.0.0, async@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==

async@^1.4.0:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=

async@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
Expand Down Expand Up @@ -10583,12 +10578,12 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=

nconf@0.11.3:
version "0.11.3"
resolved "https://registry.yarnpkg.com/nconf/-/nconf-0.11.3.tgz#4ee545019c53f1037ca57d696836feede3c49163"
integrity sha512-iYsAuDS9pzjVMGIzJrGE0Vk3Eh8r/suJanRAnWGBd29rVS2XtSgzcAo5l6asV3e4hH2idVONHirg1efoBOslBg==
nconf@0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/nconf/-/nconf-0.12.0.tgz#9cf70757aae4d440d43ed53c42f87da18471b8bf"
integrity sha512-T3fZPw3c7Dfrz8JBQEbEcZJ2s8f7cUMpKuyBtsGQe0b71pcXx6gNh4oti2xh5dxB+gO9ufNfISBlGvvWtfyMcA==
dependencies:
async "^1.4.0"
async "^3.0.0"
ini "^2.0.0"
secure-keys "^1.0.0"
yargs "^16.1.1"
Expand Down Expand Up @@ -12546,6 +12541,11 @@ range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==

rate-limiter-flexible@2.3.7:
version "2.3.7"
resolved "https://registry.yarnpkg.com/rate-limiter-flexible/-/rate-limiter-flexible-2.3.7.tgz#c23e1f818a1575f1de1fd173437f4072125e1615"
integrity sha512-dmc+J/IffVBvHlqq5/XClsdLdkOdQV/tjrz00cwneHUbEDYVrf4aUDAyR4Jybcf2+Vpn4NwoVrnnAyt/D0ciWw==

raw-body@2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c"
Expand Down

0 comments on commit 526cef3

Please sign in to comment.