recaptcha and rate limiting
This commit is contained in:
parent
6e246fe687
commit
1a389ec1dd
21
package-lock.json
generated
21
package-lock.json
generated
@ -887,6 +887,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"clone": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz",
|
||||
"integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk="
|
||||
},
|
||||
"co": {
|
||||
"version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
|
||||
@ -1236,6 +1241,14 @@
|
||||
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"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": {
|
||||
"version": "https://registry.npmjs.org/express-session/-/express-session-1.15.3.tgz",
|
||||
"integrity": "sha1-21RfBDWnsbIorgLagZf2UUFzXGc=",
|
||||
|
@ -39,6 +39,7 @@
|
||||
"connect-redis": "^3.3.0",
|
||||
"connect-session-knex": "^1.3.4",
|
||||
"express": "^4.15.3",
|
||||
"express-rate-limit": "^2.9.0",
|
||||
"express-session": "^1.15.3",
|
||||
"knex": "^0.13.0",
|
||||
"mysql": "^2.13.0",
|
||||
|
@ -153,7 +153,7 @@ const API = {
|
||||
return true
|
||||
},
|
||||
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
|
||||
if (!user.password || user.password === '') return null
|
||||
|
@ -4,6 +4,10 @@ import model from '../../model'
|
||||
module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => {
|
||||
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 (!req.body || (typeof req.body['decision']) === 'undefined') {
|
||||
throw new error.InvalidRequest('No decision parameter passed')
|
||||
|
@ -4,10 +4,14 @@ import model from '../../model'
|
||||
module.exports = async (req, res, client, scope, user, redirectUri, createAllowFuture) => {
|
||||
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 (!req.body || (typeof req.body['decision']) === 'undefined') {
|
||||
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')
|
||||
} else {
|
||||
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),
|
||||
req.oauth2.model.client.getId(client), scope, req.oauth2.model.accessToken.ttl)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new error.ServerError('Failed to call accessToken.create function')
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import express from 'express'
|
||||
import parseurl from 'parseurl'
|
||||
import RateLimit from 'express-rate-limit'
|
||||
import config from '../../scripts/load-config'
|
||||
import wrap from '../../scripts/asyncRoute'
|
||||
import API from '../api'
|
||||
@ -7,6 +8,14 @@ import APIExtern from '../api/external'
|
||||
|
||||
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
|
||||
function objectAssembler (insane) {
|
||||
let object = {}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import RateLimit from 'express-rate-limit'
|
||||
import parseurl from 'parseurl'
|
||||
import config from '../../scripts/load-config'
|
||||
import wrap from '../../scripts/asyncRoute'
|
||||
import http from '../../scripts/http'
|
||||
import API from '../api'
|
||||
|
||||
import apiRouter from './api'
|
||||
@ -11,6 +13,13 @@ import oauthRouter from './oauth2'
|
||||
|
||||
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) => {
|
||||
let messages = req.flash('message')
|
||||
if (!messages || !messages.length) {
|
||||
@ -30,11 +39,28 @@ router.use('/oauth2', oauthRouter)
|
||||
RENDER VIEWS
|
||||
================
|
||||
*/
|
||||
router.get('/', wrap(async (req, res) => {
|
||||
router.get('/', (req, res) => {
|
||||
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) {
|
||||
let uri = '/'
|
||||
if (req.session.redirectUri) {
|
||||
@ -45,22 +71,10 @@ router.get('/login', wrap(async (req, res) => {
|
||||
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')
|
||||
}))
|
||||
})
|
||||
|
||||
router.get('/register', wrap(async (req, res) => {
|
||||
router.get('/register', extraButtons, (req, res) => {
|
||||
if (req.session.user) return res.redirect('/')
|
||||
|
||||
let dataSave = req.flash('formkeep')
|
||||
@ -71,20 +85,13 @@ router.get('/register', wrap(async (req, res) => {
|
||||
}
|
||||
|
||||
res.locals.formkeep = dataSave
|
||||
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
|
||||
if (config.security.recaptcha && config.security.recaptcha.site_key) {
|
||||
res.locals.recaptcha = config.security.recaptcha.site_key
|
||||
}
|
||||
|
||||
res.render('register')
|
||||
}))
|
||||
})
|
||||
|
||||
router.get('/user/two-factor', wrap(async (req, res) => {
|
||||
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')
|
||||
}))
|
||||
|
||||
router.get('/login/verify', wrap(async (req, res) => {
|
||||
router.get('/login/verify', (req, res) => {
|
||||
res.render('totp-check')
|
||||
}))
|
||||
})
|
||||
|
||||
/*
|
||||
=================
|
||||
@ -117,7 +124,7 @@ router.get('/login/verify', wrap(async (req, res) => {
|
||||
*/
|
||||
|
||||
function formError (req, res, error, redirect) {
|
||||
// Security measures
|
||||
// Security measures: never store any passwords in any session
|
||||
if (req.body.password) {
|
||||
delete req.body.password
|
||||
if (req.body.password_repeat) {
|
||||
@ -254,7 +261,7 @@ router.post('/login', wrap(async (req, res) => {
|
||||
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) {
|
||||
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!')
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
let hash = await API.User.Register.hashPassword(password)
|
||||
|
||||
// Attempt to create the user
|
||||
@ -323,7 +347,7 @@ router.post('/register', wrap(async (req, res) => {
|
||||
*/
|
||||
|
||||
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')
|
||||
if (!fs.existsSync(docsDir) || !fs.existsSync(doc)) {
|
||||
return res.status(404).end()
|
||||
@ -332,7 +356,7 @@ router.get('/docs/:name', wrap(async (req, res) => {
|
||||
doc = fs.readFileSync(doc, {encoding: 'utf8'})
|
||||
|
||||
res.render('document', {doc: doc})
|
||||
}))
|
||||
})
|
||||
|
||||
/*
|
||||
=========
|
||||
|
@ -1,6 +1,7 @@
|
||||
import express from 'express'
|
||||
import uapi from '../api'
|
||||
import OAuth2 from '../api/oauth2'
|
||||
import RateLimit from 'express-rate-limit'
|
||||
import config from '../../scripts/load-config'
|
||||
import wrap from '../../scripts/asyncRoute'
|
||||
|
||||
@ -9,6 +10,14 @@ let oauth = new OAuth2()
|
||||
|
||||
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) {
|
||||
if (req.session.user) {
|
||||
next()
|
||||
|
@ -169,6 +169,9 @@ footer
|
||||
// display: block
|
||||
// left: 40%
|
||||
|
||||
.g-recaptcha
|
||||
margin-top: 10px
|
||||
|
||||
ul.flexview
|
||||
position: fixed
|
||||
right: 0
|
||||
|
@ -27,6 +27,9 @@ block body
|
||||
label(for="password_repeat") Repeat Password
|
||||
input(type="password", name="password_repeat", id="password_repeat")
|
||||
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")
|
||||
a#create(href="/login") Log in with an existing account
|
||||
.right
|
||||
|
@ -6,8 +6,8 @@ block body
|
||||
.wrapper
|
||||
.boxcont
|
||||
.box#login
|
||||
h1 Two Factor Authentication
|
||||
.left
|
||||
h1 Two Factor Authentication
|
||||
if message
|
||||
if message.error
|
||||
.message.error
|
||||
|
Reference in New Issue
Block a user