From a58f43e47ef8105f41a7fdae060e1a691a9c8c26 Mon Sep 17 00:00:00 2001 From: Florian Schmidt Date: Tue, 18 Apr 2017 20:59:52 +0200 Subject: [PATCH] Add password complexity checks for openhab-cloud account All places, where a password can be set will now go through a new class, UserPassword, which will check the password complexity before setting the password to the user account. It also provides a check method to check the complexity requirements before a user account will be created. If the complexity rules aren't met, the save of the password will be denied and an error message with the complexity rules is shown. Fixes #75 --- routes/account.js | 31 ++++++++++---- routes/users.js | 6 +++ userpassword.js | 102 ++++++++++++++++++++++++++++++++++++++++++++++ views/alerts.ejs | 8 ++++ 4 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 userpassword.js diff --git a/routes/account.js b/routes/account.js index 3653929..3092a30 100644 --- a/routes/account.js +++ b/routes/account.js @@ -24,6 +24,7 @@ var uuid = require('uuid'); var mailer = require('../mailer'); var logger = require('../logger'); var app = require('../app'); +var UserPassword = require('../userpassword'); exports.lostpasswordget = function(req, res) { res.render('lostpassword', {title: "Lost my password", user: req.user, @@ -102,8 +103,10 @@ exports.lostpasswordresetpost = function(req, res) { if (lostPassword && !error) { User.findOne({_id: lostPassword.user}, function(error, lostUser) { if (lostUser && !error) { - lostUser.password = req.body.password; - lostUser.save(function(error) { + var userPassword, result; + + userPassword = new UserPassword(lostUser); + result = userPassword.setPassword(req.body.password, function(error) { if (!error) { lostPassword.used = true; lostPassword.save(); @@ -114,6 +117,11 @@ exports.lostpasswordresetpost = function(req, res) { res.redirect('/'); } }); + + if (!result) { + UserPassword.printPasswordNotComplexEnoughError(req); + res.redirect('/lostpasswordreset?resetCode=' + req.body.resetCode); + } } else { req.flash('error', 'There was an error while processing your request'); res.redirect('/'); @@ -179,11 +187,14 @@ exports.accountpasswordpost = function(req, res) { res.redirect('/account'); } else { if (req.body.password == req.body.password1) { - user = req.user; - user.password = req.body.password; - user.save(); - req.flash('info', 'Password successfully changed'); - res.redirect('/account'); + userPassword = new UserPassword(req.user); + if (!userPassword.setPassword(req.body.password)) { + UserPassword.printPasswordNotComplexEnoughError(req); + res.redirect('/account'); + } else { + req.flash('info', 'Password successfully changed'); + res.redirect('/account'); + } } else { req.flash('error', 'Passwords don\'t match'); res.redirect('/account'); @@ -343,6 +354,12 @@ exports.registerpost = function(req, res) { res.render('login', { title: "Login / Sign up", user: req.user, errormessages:req.flash('error'), infomessages:req.flash('info') }); } else { + if (!UserPassword.isComplexEnough(req.body.password)) { + UserPassword.printPasswordNotComplexEnoughError(req); + res.render('login', { title: "Login / Sign up", user: req.user, + errormessages:req.flash('error'), infomessages:req.flash('info') }); + return; + } User.register(req.body.username, req.body.password, function(err, user) { if (err) { req.flash('error', "An error occured during registration, please contact support"); diff --git a/routes/users.js b/routes/users.js index 0c4c3d9..c01929d 100644 --- a/routes/users.js +++ b/routes/users.js @@ -2,6 +2,7 @@ var User = require('../models/user'); var Openhab = require('../models/openhab'); var UserDevice = require('../models/userdevice'); var logger = require('../logger'); +var UserPassword = require('../userpassword'); var mongoose = require('mongoose'), Schema = mongoose.Schema; var ObjectId = mongoose.SchemaTypes.ObjectId; @@ -60,6 +61,11 @@ exports.usersaddpost = function(req, res) { res.redirect('/users/add'); } else { if (req.body.password == req.body.password1) { + if (!UserPassword.isComplexEnough(req.body.password)) { + UserPassword.printPasswordNotComplexEnoughError(req); + res.redirect('/users/add'); + return; + } User.findOne({username: req.body.username}, function(error, checkUser) { if (!error) { if (checkUser) { diff --git a/userpassword.js b/userpassword.js new file mode 100644 index 0000000..e01867b --- /dev/null +++ b/userpassword.js @@ -0,0 +1,102 @@ +var User = require('./models/user'); + +/** + * A helper class for setting the password of an arbitrary user. + * + * @param user The user where the password change attempts should happen + * @constructor + */ +var UserPassword = function(user) { + if (!(user instanceof User)) { + throw new Error('UserPassword can only be instantiated with an User object, ' + + user.constructor + ' given.'); + } + this.user = user; +}; + +/** + * Verifies the password complexity rules for the given plain text password and, if it + * matches the requirements, sets the new password to the given user. + * + * The complexity rules includes: + * - at least 4 characters + * - has at least two characters of the following categories: uppercase letters, lowercase letters, numbers, special characters + * + * @param {string} password + * @param fn Optional function to pass to the save function of User + * @return {boolean} True, if saving the password was initiated, false otherwise. False is garantueed to mean, that the + * password does not meet the complexity requirements. + */ +UserPassword.prototype.setPassword = function(password, fn) { + if (!UserPassword.isComplexEnough(password)) { + return false; + } + + this.user.password = password; + if (typeof fn !== 'function') { + fn = undefined; + } + this.user.save(fn); + + return true; +}; + +/** + * Helper function for checking the complexity of a password. + * + * @param password + * @returns {boolean} + */ +UserPassword.isComplexEnough = function(password) { + var matches = 0; + + if (password.length < 4) { + return false; + } + + // match against all lowercase letters + if (password.match(/^(?=.*[a-z]).+$/)) { + matches++; + } + + // match against all uppercase letters + if (password.match(/^(?=.*[A-Z]).+$/)) { + matches++; + } + + // match against all numbers + if (password.match(/^(?=.*\d).+$/)) { + matches++; + } + + // match against a reasonable set of special characters + if (password.match(/^(?=.*[-+_!@#$%^&*.,?]).+$/)) { + matches++; + } + + if (matches < 2) { + return false; + } + + return true; +}; + +/** + * Prints a message to the given request, that the entered password was not complex enough, while giving the complexity + * rules for passwords. + * + * @param req + */ +UserPassword.printPasswordNotComplexEnoughError = function(req) { + var message = []; + message.push('The password does not meet the password complexity requirements. Please try again.'); + message.push('Your password must be at least 4 characters long and need to contain at least 2 different characters from the following groups:'); + message.push(' * Lowercase letters (a, b, c, ...)'); + message.push(' * Uppercase letters (A, B, C, ...)'); + message.push(' * Numbers (1, 2, 3, ...)'); + message.push(' * Special characters out of: -+_!@#$%^&*.,?'); + + req.flash('error', message); +}; + +module.exports = UserPassword; diff --git a/views/alerts.ejs b/views/alerts.ejs index fda4e3e..301360a 100644 --- a/views/alerts.ejs +++ b/views/alerts.ejs @@ -3,7 +3,15 @@
+ <% if(errormessages.length === 1) { %> <%= errormessages %> + <% } else { %> +
    + <% errormessages.forEach(function(err) { %> +
  • <%= err %>
  • + <% }) %> +
+ <% } %>