usable authentication and session management

This commit is contained in:
Jörn-Michael Miehe 2018-12-16 22:51:08 +01:00
parent 4f143edc23
commit 1f08303947
6 changed files with 136 additions and 61 deletions

View file

@ -16,10 +16,10 @@ FFTCGDB = (filename) ->
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
login text NOT NULL COLLATE NOCASE, login text NOT NULL COLLATE NOCASE,
pwdhash text NOT NULL, pwdhash text NOT NULL,
session text,
UNIQUE(login) UNIQUE(login)
); );
""" """
console.log "[FFTCGDB] Connected to '#{@filename}'" console.log "[FFTCGDB] Connected to '#{@filename}'"
return return
@ -35,46 +35,74 @@ FFTCGDB::register = (login, password) ->
that = @ that = @
new Promise (resolve, reject) -> new Promise (resolve, reject) ->
# validate username # validate user input
if login == '' or password == ''
# no user name or password given
console.log "[FFTCGDB] reg: user name '#{login}' or password empty"
reject 'invalid'
# hash password # hash password
bcrypt.hash password, saltRounds, (err, hash) -> bcrypt.hash password, saltRounds, (err, hash) ->
reject 'hash' if err
# try creating row in users table
that.db.run "INSERT INTO users (login, pwdhash) VALUES ('#{login}', '#{hash}');", (err) ->
if err if err
if err.code == 'SQLITE_CONSTRAINT' console.log "[FFTCGDB] reg: hash fail for name '#{login}'"
reject 'existence' reject 'hash'
else
reject 'db' # try creating row in users table
stmt = that.db.prepare 'INSERT INTO users (login, pwdhash) VALUES (?, ?)'
stmt.run [login, hash], (err) ->
if err
console.log "[FFTCGDB] reg: DB fail '#{err.code}' for name '#{login}'"
stmt.finalize()
# reduce attack surface, don't disclose user names
reject 'db' # user already exists
else else
console.log "[FFTCGDB] reg: OK '#{login}'"
stmt.finalize()
# registration successful # registration successful
resolve @lastID resolve
uid: @lastID
login: login
FFTCGDB::login = (login, password) -> FFTCGDB::login = (login, password) ->
that = @ that = @
new Promise (resolve, reject) -> new Promise (resolve, reject) ->
# validate username
# get users table row # get users table row
that.db.all "SELECT rowid, pwdhash FROM users WHERE login = '#{login}';", (err, rows) -> stmt = that.db.prepare 'SELECT rowid, login, pwdhash FROM users WHERE login = ?'
stmt.get [login], (err, row) ->
if err if err
console.log "[FFTCGDB] login: DB fail '#{err.code}' for name '#{login}'"
stmt.finalize()
reject 'db' reject 'db'
else if rows.length == 0 else if not row
# hashing the password for timing attack reasons # hash the password for timing attack reasons
bcrypt.hash password, saltRounds, (err, hash) -> bcrypt.hash password, saltRounds, (err, hash) ->
reject 'existence' console.log "[FFTCGDB] login: nonexistent '#{login}'"
stmt.finalize()
# reduce attack surface, don't disclose user names
reject 'login' # user doesnt exist
else else
row = rows[0]
bcrypt.compare password, row.pwdhash, (err, res) -> bcrypt.compare password, row.pwdhash, (err, res) ->
reject 'hash' if err if err
console.log "[FFTCGDB] login: hash fail for name '#{login}'"
reject 'hash'
if res == true if res == true
resolve row.rowid console.log "[FFTCGDB] login: OK '#{row.login}'"
stmt.finalize()
# login successful
resolve
uid: row.rowid
login: row.login
else else
console.log "[FFTCGDB] login: wrong password for '#{login}'"
stmt.finalize()
# login failed
reject 'login' reject 'login'
module.exports = FFTCGDB module.exports = FFTCGDB

View file

@ -1,5 +1,5 @@
# node libraries # node libraries
FFTCGROUTER = (require 'express').Router() express = (require 'express')
path = (require 'path') path = (require 'path')
# my libraries # my libraries
@ -8,17 +8,22 @@ FFTCGDB = (require './fftcgdb')
# open fftcg db # open fftcg db
fftcgdb = new FFTCGDB path.resolve(__dirname, '../fftcg.db') fftcgdb = new FFTCGDB path.resolve(__dirname, '../fftcg.db')
# create router
FFTCGROUTER = express.Router()
FFTCGROUTER.use express.static path.resolve(__dirname, '../public_html')
# register user # register user
FFTCGROUTER.post '/register', (req, res) -> FFTCGROUTER.post '/register', (req, res) ->
fftcgdb.register req.body.login, req.body.password fftcgdb.register req.body.login, req.body.password
.then (userid) -> .then (user) ->
console.log "registered '#{req.body.login}'" # registration successful, return JSON status
res.json res.json
status: 'ok' status: 'ok'
uid: userid uid: user.id
text: req.body.login login: user.login
.catch (err) -> .catch (err) ->
console.log "failed to register '#{req.body.login}'" # registration failed, return JSON status
res.json res.json
status: 'fail' status: 'fail'
text: err text: err
@ -26,17 +31,19 @@ FFTCGROUTER.post '/register', (req, res) ->
# log in user # log in user
FFTCGROUTER.post '/login', (req, res) -> FFTCGROUTER.post '/login', (req, res) ->
fftcgdb.login req.body.login, req.body.password fftcgdb.login req.body.login, req.body.password
.then (userid) -> .then (user) ->
req.session.userID = userid # login successful, save stuff in session
req.session.userLogin = req.body.login req.session.user = user
req.session.save() req.session.save()
console.log "logged in '#{req.body.login}'"
# return JSON status
res.json res.json
status: 'ok' status: 'ok'
uid: userid uid: user.uid
text: req.body.login login: user.login
.catch (err) -> .catch (err) ->
console.log "failed to login '#{req.body.login}'" # login failed, return JSON status
res.json res.json
status: 'fail' status: 'fail'
text: err text: err

View file

@ -21,17 +21,14 @@ app.use bodyParser.urlencoded
sessionMiddleware = FFTCGSESSION(app) sessionMiddleware = FFTCGSESSION(app)
app.use sessionMiddleware app.use sessionMiddleware
# REST routes # routes
app.use FFTCGROUTER app.use FFTCGROUTER
# Static content
app.use express.static path.resolve(__dirname, 'public_html')
# Templates # Templates
app.set 'view engine', 'pug' app.set 'view engine', 'pug'
app.get '/:template.html', (req, res) -> app.get '/:template.html', (req, res) ->
if req.session if req.session.user
console.log "logged in as '#{req.session.userLogin}'" console.log "[FFTCG] user is '#{req.session.user.login}'"
res.render (req.params.template + '.pug') res.render (req.params.template + '.pug')
# socket.io # socket.io

View file

@ -1,23 +1,39 @@
# libs # libs
window.$ = require('jquery') window.$ = require('jquery')
# import bootstrap
require './style/custom.scss'
require 'bootstrap/js/dist/alert'
window.showAlert = (level, content) ->
($ '.alert').alert 'close'
($ '#alert-area').append ($ '<div>',
class: "alert alert-#{level} alert-dismissible fade show"
role: 'alert'
.append content, ($ '<button>',
type: 'button'
class: 'close'
'data-dismiss': 'alert',
'aria-label': 'Close'
.append ($ '<span>',
'aria-hidden': 'true'
.append '&times;'
)))
# on load # on load
$ -> $ ->
# style sheet
require './style/custom.scss'
# reset forms # reset forms
$('form').each -> $('form').each ->
that = @
@fullReset = -> @fullReset = ->
$('input', that).each -> $('input', @).each ->
$(that).removeClass 'is-invalid' $(@).removeClass 'is-invalid'
$(that).removeClass 'is-valid' $(@).removeClass 'is-valid'
that.reset() @reset()
# login form # login form
$('form[name="login"]').submit (event) -> $('form[name="login"]').submit (event) ->
that = @
# inhibit normal form submission # inhibit normal form submission
event.preventDefault() event.preventDefault()
@ -30,13 +46,24 @@ $ ->
login: login.val() login: login.val()
password: password.val() password: password.val()
.done (data) -> .done (data) ->
alert "#{data.status}, #{data.uid}, #{data.text}" if data.status == 'ok'
that.fullReset()
showAlert 'success', "successfully logged in '#{data.login}'"
# reset form else
@fullReset() switch data.text
when 'login'
showAlert 'warning', 'Invalid username and/or password.'
login.addClass 'is-invalid'
password.addClass 'is-invalid'
when 'db' or 'hash'
showAlert 'danger', 'Internal failure, try again later.'
else
showAlert 'danger', 'Unknown failure. Can you reproduce this?'
# registration form # registration form
$('form[name="register"]').submit (event) -> $('form[name="register"]').submit (event) ->
that = @
# inhibit normal form submission # inhibit normal form submission
event.preventDefault() event.preventDefault()
@ -46,19 +73,32 @@ $ ->
confirm = $('input[name="confirm"]', @) confirm = $('input[name="confirm"]', @)
# check form data # check form data
if password.val() == confirm.val() if password.val() != confirm.val()
confirm.addClass 'is-invalid'
confirm.focus()
else
# transmit form data # transmit form data
$.post '/register', $.post '/register',
login: login.val() login: login.val()
password: password.val() password: password.val()
.done (data) -> .done (data) ->
alert "#{data.status}, #{data.uid}, #{data.text}" if data.status == 'ok'
that.fullReset()
# reset form showAlert 'success', "successfully registered '#{data.login}'"
@fullReset()
else else
confirm.val '' switch data.text
confirm.addClass 'is-invalid' when 'invalid'
confirm.focus() showAlert 'warning', 'Invalid user input. Please provide username AND password.'
login.addClass 'is-invalid'
password.addClass 'is-invalid'
login.focus()
when 'hash'
showAlert 'danger', 'Internal failure, try again later.'
when 'db'
showAlert 'danger', 'Internal failure or user name already taken.'
login.addClass 'is-invalid'
login.focus()
else
showAlert 'danger', 'Unknown failure. Can you reproduce this?'

View file

@ -2,7 +2,7 @@
// Bootstrap and its default variables // Bootstrap and its default variables
// //
@import "node_modules/bootstrap/scss/bootstrap"; @import "~bootstrap/scss/bootstrap";
html, body { html, body {
height: 100%; height: 100%;

View file

@ -10,9 +10,10 @@ html
header.jumbotron.jumbotron-fluid.py-4.bg-primary.text-light.text-center header.jumbotron.jumbotron-fluid.py-4.bg-primary.text-light.text-center
div.container div.container
h1 Hello World! h1 Hello World!
h2 App under development, please don't send valuable data! h2 App under development, please don't submit any valuable data!
div.container.bg-light div.container.bg-light
div#alert-area
div.row div.row
div.col-md-6 div.col-md-6
@ -39,12 +40,14 @@ html
div.form-group div.form-group
label(for="login") User name: label(for="login") User name:
input.form-control(name="login" required) input.form-control(name="login" required)
div.invalid-feedback User name not available. div.invalid-feedback User name invalid or taken.
div.form-group div.form-group
label(for="password") Password: label(for="password") Password:
input.form-control(name="password" type="password" required) input.form-control(name="password" type="password" required)
label(for="confirm") Confirm password: label(for="confirm") Confirm password:
input.form-control(name="confirm" type="password") input.form-control(name="confirm" type="password")
div.invalid-feedback Passwords do not match.
button.btn.btn-primary.w-100(type="submit") Register button.btn.btn-primary.w-100(type="submit") Register