This commit is contained in:
Evert Prants 2017-08-03 01:35:10 +03:00
parent 5536ecbf58
commit 298f9bf818
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
7 changed files with 282 additions and 16 deletions

View File

@ -9,7 +9,7 @@ exports.up = function(knex, Promise) {
table.string('email').notNullable()
table.string('avatar_file')
table.text('password').notNullable()
table.text('password')
table.boolean('activated').defaultTo(false)
table.boolean('locked').defaultTo(false)

View File

@ -4,6 +4,8 @@ import config from '../../scripts/load-config'
import database from '../../scripts/load-database'
import models from './models'
import crypto from 'crypto'
import notp from 'notp'
import base32 from 'thirty-two'
const emailRe = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
@ -32,7 +34,23 @@ function bcryptTask (data) {
})
}
function keysAvailable (object, required) {
let found = true
for (let i in required) {
let key = required[i]
if (object[key] == null) {
found = false
}
}
return found
}
const API = {
Hash: (len) => {
return crypto.randomBytes(len).toString('hex')
},
User: {
get: async function (identifier) {
let scope = 'id'
@ -57,8 +75,8 @@ const API = {
return user[0]
},
ensureObject: async function (user) {
if (!typeof user === 'object') {
ensureObject: async function (user, fieldsPresent = ['id']) {
if (typeof user !== 'object' || !keysAvailable(user, fieldsPresent)) {
return await API.User.get(user)
}
@ -84,6 +102,78 @@ const API = {
await models.User.query().patchAndFetchById(user.id, {activated: 1})
await models.Token.query().delete().where('id', getToken[0].id)
return true
},
totpTokenRequired: async function (user) {
let getToken = await models.TotpToken.query().where('user_id', user.id)
if (!getToken || !getToken.length) return false
if (getToken[0].activated !== 1) return false
return true
},
totpCheck: async function (user, code, emerg) {
user = await API.User.ensureObject(user)
let getToken = await models.TotpToken.query().where('user_id', user.id)
if (!getToken || !getToken.length) return false
getToken = getToken[0]
if (emerg) {
if (emerg === getToken.recovery_code) {
return true
}
return false
}
let login = notp.totp.verify(code, getToken.token, {})
if (login) {
if (login.delta !== 0) {
return false
}
if (getToken.activated !== 1) {
// TODO: Send an email including the recovery code to the user
await models.TotpToken.query().patchAndFetchById(getToken.id, {activated: true})
}
return true
}
return false
},
purgeTotp: async function (user, password) {
user = await API.User.ensureObject(user, ['password'])
let pwmatch = await API.User.Login.password(user, password)
if (!pwmatch) return false
// TODO: Inform user via email
await models.TotpToken.query().delete().where('user_id', user.id)
return true
},
totpAquire: async function (user) {
user = await API.User.ensureObject(user)
let getToken = await models.TotpToken.query().where('user_id', user.id)
if (getToken && getToken.length) {
await models.TotpToken.query().delete().where('user_id', user.id)
}
let newToken = {
user_id: user.id,
token: API.Hash(16),
recovery_code: API.Hash(8),
created_at: new Date()
}
let hashed = base32.encode(newToken.token)
let domain = 'icynet.eu'
await models.TotpToken.query().insert(newToken)
let uri = encodeURIComponent('otpauth://totp/' + user.username + '@' + domain + '?secret=' + hashed)
return uri
}
},
Register: {
@ -112,7 +202,7 @@ const API = {
let user = await models.User.query().insert(data)
// Activation token
let activationToken = crypto.randomBytes(16).toString('hex')
let activationToken = API.Hash(16)
await models.Token.query().insert({
expires_at: new Date(Date.now() + 86400000), // 1 day
token: activationToken,

View File

@ -56,6 +56,29 @@ router.get('/register', wrap(async (req, res) => {
res.render('register')
}))
router.get('/user/two-factor', wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
if (twoFaEnabled) return res.redirect('/')
let newToken = await API.User.Login.totpAquire(req.session.user)
res.locals.uri = newToken
res.render('totp')
}))
router.get('/user/two-factor/disable', wrap(async (req, res) => {
if (!req.session.user) return res.redirect('/login')
let twoFaEnabled = await API.User.Login.totpTokenRequired(req.session.user)
if (!twoFaEnabled) return res.redirect('/')
res.render('password')
}))
router.get('/login/verify', wrap(async (req, res) => {
res.render('totp-check')
}))
/*
=================
POST HANDLING
@ -68,6 +91,80 @@ function formError (req, res, error, path) {
res.redirect(path || parseurl(req).path)
}
router.post('/user/two-factor', wrap(async (req, res) => {
if (!req.body.code) {
return formError(req, res, 'You need to enter the code.')
}
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
let verified = await API.User.Login.totpCheck(req.session.user, req.body.code)
if (!verified) {
return formError(req, res, 'Try again!')
}
res.redirect('/')
}))
router.post('/user/two-factor/disable', wrap(async (req, res) => {
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
if (!req.body.password) {
return formError(req, res, 'Please enter your password.')
}
let purge = await API.User.Login.purgeTotp(req.session.user, req.body.password)
if (!purge) {
return formError(req, res, 'Invalid password.')
}
res.redirect('/')
}))
router.post('/login/verify', wrap(async (req, res) => {
if (req.session.totp_check === null) return res.redirect('/login')
if (!req.body.code && !req.body.recovery) {
return formError(req, res, 'You need to enter the code.')
}
if (req.body.csrf !== req.session.csrf) {
return formError(req, res, 'Invalid session! Try reloading the page.')
}
let totpCheck = await API.User.Login.totpCheck(req.session.totp_check, req.body.code, req.body.recovery || false)
if (!totpCheck) {
return formError(req, res, 'Invalid code!')
}
let user = await API.User.get(req.session.totp_check)
delete req.session.totp_check
// Set session
req.session.user = {
id: user.id,
username: user.username,
display_name: user.display_name,
email: user.email,
avatar_file: user.avatar_file
}
let uri = '/'
if (req.session.redirectUri) {
uri = req.session.redirectUri
delete req.session.redirectUri
}
if (req.query.redirect) {
uri = req.query.redirect
}
res.redirect(uri)
}))
router.post('/login', wrap(async (req, res) => {
if (!req.body.username || !req.body.password) {
return res.redirect('/login')
@ -86,7 +183,12 @@ router.post('/login', wrap(async (req, res) => {
if (user.activated === 0) return formError(req, res, 'Please activate your account first.')
if (user.locked === 1) return formError(req, res, 'This account has been locked.')
// TODO: TOTP checks
let totpRequired = await API.User.Login.totpTokenRequired(user)
if (totpRequired) {
req.session.totp_check = user.id
return res.redirect('/login/verify')
}
// TODO: Ban checks
// Set session
@ -173,17 +275,6 @@ router.post('/register', wrap(async (req, res) => {
res.redirect('/login')
}))
router.get('/activate/:token', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login')
let token = req.params.token
let success = await API.User.Login.activationToken(token)
if (!success) return formError(req, res, 'Unknown or invalid activation token')
req.flash('message', {error: false, text: 'Your account has been activated! You may now log in.'})
res.redirect('/login')
}))
/*
=========
OTHER
@ -198,6 +289,17 @@ router.get('/logout', wrap(async (req, res) => {
res.redirect('/')
}))
router.get('/activate/:token', wrap(async (req, res) => {
if (req.session.user) return res.redirect('/login')
let token = req.params.token
let success = await API.User.Login.activationToken(token)
if (!success) return formError(req, res, 'Unknown or invalid activation token')
req.flash('message', {error: false, text: 'Your account has been activated! You may now log in.'})
res.redirect('/login')
}))
router.use((err, req, res, next) => {
console.error(err)
next()

View File

@ -129,6 +129,8 @@ input[type="submit"]
width: 46%
&#login
max-width: 700px
&#totpcheck
max-width: 400px
padding: 20px
margin: auto
margin-top: 5%

21
views/password.pug Normal file
View File

@ -0,0 +1,21 @@
extends layout.pug
block title
|Icy Network - Password Required
block body
.wrapper
.boxcont
.box#totpcheck
h1 Enter your password
small.descr This action requires your password to continue
if message
if message.error
.message.error
else
.message
span #{message.text}
form#loginForm(method="POST", action="")
input(type="hidden", name="csrf", value=csrf)
label(for="password") Password
input(type="password", name="password", id="password")
input(type="submit", value="Continue")

21
views/totp-check.pug Normal file
View File

@ -0,0 +1,21 @@
extends layout.pug
block title
|Icy Network - Log In - Verification Required
block body
.wrapper
.boxcont
.box#totpcheck
h1 Enter Code
small.descr This user has Two Factor Authentication enabled. Enter the code to log in.
if message
if message.error
.message.error
else
.message
span #{message.text}
form#loginForm(method="POST", action="")
input(type="hidden", name="csrf", value=csrf)
label(for="code") Code
input(type="text", name="code", id="code")
input(type="submit", value="Log in")

30
views/totp.pug Normal file
View File

@ -0,0 +1,30 @@
extends layout.pug
block title
|Icy Network - Activate Authenticator
block body
.wrapper
.boxcont
.box#login
.left
h1 Two Factor Authentication
if message
if message.error
.message.error
else
.message
span #{message.text}
img.qr(src="//api.qrserver.com/v1/create-qr-code/?data=" + uri)
form#totpForm(method="POST", action="")
input(type="hidden", name="csrf", value=csrf)
label(for="code") Enter the Code
input(type="text", name="code", id="code")
input(type="submit", value="Enable Now")
.right
h3 How to use
ol
li Scan the QR Code with your authenticator app
li Enter the one-time code given to you
li You will now be asked for a code every time you log in
h3 Authenticator app
p We recommend using Google Authenticator