recaptcha and rate limiting

This commit is contained in:
Evert Prants 2017-08-24 01:25:52 +03:00
parent 6e246fe687
commit 1a389ec1dd
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
11 changed files with 117 additions and 38 deletions

21
package-lock.json generated
View File

@ -887,6 +887,11 @@
} }
} }
}, },
"clone": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz",
"integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk="
},
"co": { "co": {
"version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
@ -1236,6 +1241,14 @@
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true "dev": true
}, },
"defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
"requires": {
"clone": "1.0.2"
}
},
"define-properties": { "define-properties": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz",
@ -1785,6 +1798,14 @@
} }
} }
}, },
"express-rate-limit": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-2.9.0.tgz",
"integrity": "sha1-YsKfyTnXLwoDqHQoxkf8TuDRWXg=",
"requires": {
"defaults": "1.0.3"
}
},
"express-session": { "express-session": {
"version": "https://registry.npmjs.org/express-session/-/express-session-1.15.3.tgz", "version": "https://registry.npmjs.org/express-session/-/express-session-1.15.3.tgz",
"integrity": "sha1-21RfBDWnsbIorgLagZf2UUFzXGc=", "integrity": "sha1-21RfBDWnsbIorgLagZf2UUFzXGc=",

View File

@ -39,6 +39,7 @@
"connect-redis": "^3.3.0", "connect-redis": "^3.3.0",
"connect-session-knex": "^1.3.4", "connect-session-knex": "^1.3.4",
"express": "^4.15.3", "express": "^4.15.3",
"express-rate-limit": "^2.9.0",
"express-session": "^1.15.3", "express-session": "^1.15.3",
"knex": "^0.13.0", "knex": "^0.13.0",
"mysql": "^2.13.0", "mysql": "^2.13.0",

View File

@ -153,7 +153,7 @@ const API = {
return true return true
}, },
totpAquire: async function (user) { totpAquire: async function (user) {
user = await API.User.ensureObject(user) user = await API.User.ensureObject(user, ['password'])
// Do not allow totp for users who have registered using an external service // Do not allow totp for users who have registered using an external service
if (!user.password || user.password === '') return null if (!user.password || user.password === '') return null

View File

@ -4,6 +4,10 @@ import model from '../../model'
module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => { module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => {
let codeValue = null let codeValue = null
if (req.method === 'POST' && req.session.csrf && !(req.body.csrf && req.body.csrf === req.session.csrf)) {
throw new error.InvalidRequest('Invalid session')
}
if (createAllowFuture) { if (createAllowFuture) {
if (!req.body || (typeof req.body['decision']) === 'undefined') { if (!req.body || (typeof req.body['decision']) === 'undefined') {
throw new error.InvalidRequest('No decision parameter passed') throw new error.InvalidRequest('No decision parameter passed')

View File

@ -4,10 +4,14 @@ import model from '../../model'
module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => { module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => {
let accessTokenValue = null let accessTokenValue = null
if (req.method === 'POST' && req.session.csrf && !(req.body.csrf && req.body.csrf === req.session.csrf)) {
throw new error.InvalidRequest('Invalid session')
}
if (createAllowFuture) { if (createAllowFuture) {
if (!req.body || (typeof req.body['decision']) === 'undefined') { if (!req.body || (typeof req.body['decision']) === 'undefined') {
throw new error.InvalidRequest('No decision parameter passed') throw new error.InvalidRequest('No decision parameter passed')
} else if (req.body['decision'] === 0) { } else if (req.body['decision'] === '0') {
throw new error.AccessDenied('User denied access to the resource') throw new error.AccessDenied('User denied access to the resource')
} else { } else {
console.debug('Decision check passed') console.debug('Decision check passed')
@ -20,6 +24,7 @@ module.exports = async (req, res, client, scope, user, redirectUri, createAllowF
accessTokenValue = await req.oauth2.model.accessToken.create(req.oauth2.model.user.getId(user), accessTokenValue = await req.oauth2.model.accessToken.create(req.oauth2.model.user.getId(user),
req.oauth2.model.client.getId(client), scope, req.oauth2.model.accessToken.ttl) req.oauth2.model.client.getId(client), scope, req.oauth2.model.accessToken.ttl)
} catch (err) { } catch (err) {
console.error(err)
throw new error.ServerError('Failed to call accessToken.create function') throw new error.ServerError('Failed to call accessToken.create function')
} }

View File

@ -1,5 +1,6 @@
import express from 'express' import express from 'express'
import parseurl from 'parseurl' import parseurl from 'parseurl'
import RateLimit from 'express-rate-limit'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import wrap from '../../scripts/asyncRoute' import wrap from '../../scripts/asyncRoute'
import API from '../api' import API from '../api'
@ -7,6 +8,14 @@ import APIExtern from '../api/external'
let router = express.Router() let router = express.Router()
let apiLimiter = new RateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 100,
delayMs: 0
})
router.use(apiLimiter)
// Turn things like 'key1[key2]': 'value' into key1: {key2: 'value'} because facebook // Turn things like 'key1[key2]': 'value' into key1: {key2: 'value'} because facebook
function objectAssembler (insane) { function objectAssembler (insane) {
let object = {} let object = {}

View File

@ -1,9 +1,11 @@
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import express from 'express' import express from 'express'
import RateLimit from 'express-rate-limit'
import parseurl from 'parseurl' import parseurl from 'parseurl'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import wrap from '../../scripts/asyncRoute' import wrap from '../../scripts/asyncRoute'
import http from '../../scripts/http'
import API from '../api' import API from '../api'
import apiRouter from './api' import apiRouter from './api'
@ -11,6 +13,13 @@ import oauthRouter from './oauth2'
let router = express.Router() let router = express.Router()
let accountLimiter = new RateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
delayMs: 0,
message: 'Whoa, slow down there, buddy! You just hit our rate limits. Try again in 1 hour.'
})
router.use(wrap(async (req, res, next) => { router.use(wrap(async (req, res, next) => {
let messages = req.flash('message') let messages = req.flash('message')
if (!messages || !messages.length) { if (!messages || !messages.length) {
@ -30,11 +39,28 @@ router.use('/oauth2', oauthRouter)
RENDER VIEWS RENDER VIEWS
================ ================
*/ */
router.get('/', wrap(async (req, res) => { router.get('/', (req, res) => {
res.render('index') res.render('index')
})) })
router.get('/login', wrap(async (req, res) => { // Add social media login buttons
function extraButtons (req, res, next) {
if (config.twitter && config.twitter.api) {
res.locals.twitter_auth = true
}
if (config.discord && config.discord.api) {
res.locals.discord_auth = true
}
if (config.facebook && config.facebook.client) {
res.locals.facebook_auth = config.facebook.client
}
next()
}
router.get('/login', extraButtons, (req, res) => {
if (req.session.user) { if (req.session.user) {
let uri = '/' let uri = '/'
if (req.session.redirectUri) { if (req.session.redirectUri) {
@ -45,22 +71,10 @@ router.get('/login', wrap(async (req, res) => {
return res.redirect(uri) return res.redirect(uri)
} }
if (config.twitter && config.twitter.api) {
res.locals.twitter_auth = true
}
if (config.discord && config.discord.api) {
res.locals.discord_auth = true
}
if (config.facebook && config.facebook.client) {
res.locals.facebook_auth = config.facebook.client
}
res.render('login') res.render('login')
})) })
router.get('/register', wrap(async (req, res) => { router.get('/register', extraButtons, (req, res) => {
if (req.session.user) return res.redirect('/') if (req.session.user) return res.redirect('/')
let dataSave = req.flash('formkeep') let dataSave = req.flash('formkeep')
@ -71,20 +85,13 @@ router.get('/register', wrap(async (req, res) => {
} }
res.locals.formkeep = dataSave res.locals.formkeep = dataSave
if (config.twitter && config.twitter.api) {
res.locals.twitter_auth = true
}
if (config.discord && config.discord.api) { if (config.security.recaptcha && config.security.recaptcha.site_key) {
res.locals.discord_auth = true res.locals.recaptcha = config.security.recaptcha.site_key
}
if (config.facebook && config.facebook.client) {
res.locals.facebook_auth = config.facebook.client
} }
res.render('register') res.render('register')
})) })
router.get('/user/two-factor', wrap(async (req, res) => { router.get('/user/two-factor', wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login') if (!req.session.user) return res.redirect('/login')
@ -106,9 +113,9 @@ router.get('/user/two-factor/disable', wrap(async (req, res) => {
res.render('password') res.render('password')
})) }))
router.get('/login/verify', wrap(async (req, res) => { router.get('/login/verify', (req, res) => {
res.render('totp-check') res.render('totp-check')
})) })
/* /*
================= =================
@ -117,7 +124,7 @@ router.get('/login/verify', wrap(async (req, res) => {
*/ */
function formError (req, res, error, redirect) { function formError (req, res, error, redirect) {
// Security measures // Security measures: never store any passwords in any session
if (req.body.password) { if (req.body.password) {
delete req.body.password delete req.body.password
if (req.body.password_repeat) { if (req.body.password_repeat) {
@ -254,7 +261,7 @@ router.post('/login', wrap(async (req, res) => {
res.redirect(uri) res.redirect(uri)
})) }))
router.post('/register', wrap(async (req, res) => { router.post('/register', accountLimiter, wrap(async (req, res) => {
if (!req.body.username || !req.body.display_name || !req.body.password || !req.body.email) { if (!req.body.username || !req.body.display_name || !req.body.password || !req.body.email) {
return formError(req, res, 'Please fill in all the fields.') return formError(req, res, 'Please fill in all the fields.')
} }
@ -293,10 +300,27 @@ router.post('/register', wrap(async (req, res) => {
return formError(req, res, 'Passwords do not match!') return formError(req, res, 'Passwords do not match!')
} }
// TODO: Add reCaptcha // 6th Check: reCAPTCHA (if configuration contains key)
if (config.security.recaptcha && config.security.recaptcha.site_key) {
if (!req.body['g-recaptcha-response']) return formError(req, res, 'Please complete the reCAPTCHA!')
try {
let data = await http.POST('https://www.google.com/recaptcha/api/siteverify', {}, {
secret: config.security.recaptcha.secret_key,
response: req.body['g-recaptcha-response']
})
data = JSON.parse(data)
if (!data.success) {
return formError(req, res, 'Please complete the reCAPTCHA!')
}
} catch (e) {
console.error(e)
return formError(req, res, 'Internal server error')
}
}
// Hash the password // Hash the password
let hash = await API.User.Register.hashPassword(password) let hash = await API.User.Register.hashPassword(password)
// Attempt to create the user // Attempt to create the user
@ -323,7 +347,7 @@ router.post('/register', wrap(async (req, res) => {
*/ */
const docsDir = path.join(__dirname, '../../documents') const docsDir = path.join(__dirname, '../../documents')
router.get('/docs/:name', wrap(async (req, res) => { router.get('/docs/:name', (req, res) => {
let doc = path.join(docsDir, req.params.name + '.html') let doc = path.join(docsDir, req.params.name + '.html')
if (!fs.existsSync(docsDir) || !fs.existsSync(doc)) { if (!fs.existsSync(docsDir) || !fs.existsSync(doc)) {
return res.status(404).end() return res.status(404).end()
@ -332,7 +356,7 @@ router.get('/docs/:name', wrap(async (req, res) => {
doc = fs.readFileSync(doc, {encoding: 'utf8'}) doc = fs.readFileSync(doc, {encoding: 'utf8'})
res.render('document', {doc: doc}) res.render('document', {doc: doc})
})) })
/* /*
========= =========

View File

@ -1,6 +1,7 @@
import express from 'express' import express from 'express'
import uapi from '../api' import uapi from '../api'
import OAuth2 from '../api/oauth2' import OAuth2 from '../api/oauth2'
import RateLimit from 'express-rate-limit'
import config from '../../scripts/load-config' import config from '../../scripts/load-config'
import wrap from '../../scripts/asyncRoute' import wrap from '../../scripts/asyncRoute'
@ -9,6 +10,14 @@ let oauth = new OAuth2()
router.use(oauth.express()) router.use(oauth.express())
let oauthLimiter = new RateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 100,
delayMs: 0
})
router.use(oauthLimiter)
function ensureLoggedIn (req, res, next) { function ensureLoggedIn (req, res, next) {
if (req.session.user) { if (req.session.user) {
next() next()

View File

@ -169,6 +169,9 @@ footer
// display: block // display: block
// left: 40% // left: 40%
.g-recaptcha
margin-top: 10px
ul.flexview ul.flexview
position: fixed position: fixed
right: 0 right: 0

View File

@ -27,6 +27,9 @@ block body
label(for="password_repeat") Repeat Password label(for="password_repeat") Repeat Password
input(type="password", name="password_repeat", id="password_repeat") input(type="password", name="password_repeat", id="password_repeat")
div#repeatcheck(style="display: none") div#repeatcheck(style="display: none")
if recaptcha
script(src='https://www.google.com/recaptcha/api.js')
.g-recaptcha(data-sitekey=recaptcha)
input(type="submit", value="Register") input(type="submit", value="Register")
a#create(href="/login") Log in with an existing account a#create(href="/login") Log in with an existing account
.right .right

View File

@ -6,8 +6,8 @@ block body
.wrapper .wrapper
.boxcont .boxcont
.box#login .box#login
.left
h1 Two Factor Authentication h1 Two Factor Authentication
.left
if message if message
if message.error if message.error
.message.error .message.error