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": {
|
"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=",
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 = {}
|
||||||
|
@ -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})
|
||||||
}))
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
=========
|
=========
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -6,8 +6,8 @@ block body
|
|||||||
.wrapper
|
.wrapper
|
||||||
.boxcont
|
.boxcont
|
||||||
.box#login
|
.box#login
|
||||||
|
h1 Two Factor Authentication
|
||||||
.left
|
.left
|
||||||
h1 Two Factor Authentication
|
|
||||||
if message
|
if message
|
||||||
if message.error
|
if message.error
|
||||||
.message.error
|
.message.error
|
||||||
|
Reference in New Issue
Block a user