From 56fc9589399d9378b8dd85177dd439d902512782 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 1 Apr 2015 17:26:22 -0400 Subject: [PATCH] closes #2891 --- public/language/en_GB/reset_password.json | 3 +- public/src/client/reset.js | 2 +- src/controllers/index.js | 3 ++ src/routes/authentication.js | 58 ++++++++++++++--------- src/user/create.js | 6 ++- src/user/profile.js | 5 +- src/user/reset.js | 37 ++++++++++----- src/views/admin/settings/user.tpl | 4 ++ 8 files changed, 79 insertions(+), 39 deletions(-) diff --git a/public/language/en_GB/reset_password.json b/public/language/en_GB/reset_password.json index 96ba318a8a..9f5b0dcc45 100644 --- a/public/language/en_GB/reset_password.json +++ b/public/language/en_GB/reset_password.json @@ -12,5 +12,6 @@ "password_reset_sent": "Password Reset Sent", "invalid_email": "Invalid Email / Email does not exist!", "password_too_short": "The password entered is too short, please pick a different password.", - "passwords_do_not_match": "The two passwords you've entered do not match." + "passwords_do_not_match": "The two passwords you've entered do not match.", + "password_expired": "Your password has expired, please choose a new password" } diff --git a/public/src/client/reset.js b/public/src/client/reset.js index 568d0739a1..fcf4098b68 100644 --- a/public/src/client/reset.js +++ b/public/src/client/reset.js @@ -11,7 +11,7 @@ define('forum/reset', function() { $('#reset').on('click', function() { if (inputEl.val() && inputEl.val().indexOf('@') !== -1) { - socket.emit('user.reset.send', inputEl.val(), function(err, data) { + socket.emit('user.reset.send', inputEl.val(), function(err) { if(err) { return app.alertError(err.message); } diff --git a/src/controllers/index.js b/src/controllers/index.js index ce50cc4c17..9500fca12c 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -57,9 +57,12 @@ Controllers.reset = function(req, res, next) { } res.render('reset_code', { valid: valid, + displayExpiryNotice: req.session.passwordExpired, code: req.params.code ? req.params.code : null, breadcrumbs: helpers.buildBreadcrumbs([{text: '[[reset_password:reset_password]]', url: '/reset'}, {text: '[[reset_password:update_password]]'}]) }); + + delete req.session.passwordExpired; }); } else { res.render('reset', { diff --git a/src/routes/authentication.js b/src/routes/authentication.js index eaf09f3e8b..fd27ac7419 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -87,7 +87,7 @@ } var userslug = utils.slugify(username); - var uid; + var uid, userData = {}; async.waterfall([ function(next) { @@ -101,9 +101,12 @@ user.auth.logAttempt(uid, req.ip, next); }, function(next) { - db.getObjectFields('user:' + uid, ['password', 'banned'], next); + db.getObjectFields('user:' + uid, ['password', 'banned', 'passwordExpiry'], next); }, - function(userData, next) { + function(_userData, next) { + userData = _userData; + userData.uid = uid; + if (!userData || !userData.password) { return next(new Error('[[error:invalid-user-data]]')); } @@ -117,7 +120,7 @@ return next(new Error('[[error:invalid-password]]')); } user.auth.clearLoginAttempts(uid); - next(null, {uid: uid}, '[[success:authentication-successful]]'); + next(null, userData, '[[success:authentication-successful]]'); } ], next); }; @@ -165,6 +168,8 @@ Auth.continueLogin = function(req, res, next) { passport.authenticate('local', function(err, userData, info) { + var passwordExpiry = userData.passwordExpiry !== undefined ? parseInt(userData.passwordExpiry, 10) : null; + if (err) { return res.status(403).send(err.message); } @@ -187,28 +192,35 @@ req.session.cookie.expires = false; } - req.login({ - uid: userData.uid - }, function(err) { - if (err) { - return res.status(403).send(err.message); - } - if (userData.uid) { - user.logIP(userData.uid, req.ip); - - plugins.fireHook('action:user.loggedIn', userData.uid); - } + if (passwordExpiry && passwordExpiry < Date.now()) { + winston.verbose('[auth] Triggering password reset for uid ' + userData.uid + ' due to password policy'); + req.session.passwordExpired = true; + user.reset.generate(userData.uid, function(err, code) { + res.status(200).send(nconf.get('relative_path') + '/reset/' + code); + }); + } else { + req.login({ + uid: userData.uid + }, function(err) { + if (err) { + return res.status(403).send(err.message); + } + if (userData.uid) { + user.logIP(userData.uid, req.ip); - if (!req.session.returnTo) { - res.status(200).send(nconf.get('relative_path') + '/'); - } else { + plugins.fireHook('action:user.loggedIn', userData.uid); + } - var next = req.session.returnTo; - delete req.session.returnTo; + if (!req.session.returnTo) { + res.status(200).send(nconf.get('relative_path') + '/'); + } else { + var next = req.session.returnTo; + delete req.session.returnTo; - res.status(200).send(next); - } - }); + res.status(200).send(next); + } + }); + } })(req, res, next); }; diff --git a/src/user/create.js b/src/user/create.js index 90bd39e7d1..d11669099f 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -126,12 +126,16 @@ module.exports = function(User) { if (!data.password) { return next(); } + User.hashPassword(data.password, function(err, hash) { if (err) { return next(err); } - User.setUserField(userData.uid, 'password', hash, next); + async.parallel([ + async.apply(User.setUserField, userData.uid, 'password', hash), + async.apply(User.reset.updateExpiry, userData.uid) + ], next); }); } ], function(err) { diff --git a/src/user/profile.js b/src/user/profile.js index f16046ee4a..a284165061 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -257,7 +257,10 @@ module.exports = function(User) { return callback(err); } - User.setUserField(data.uid, 'password', hash, callback); + async.parallel([ + async.apply(User.setUserField, data.uid, 'password', hash), + async.apply(User.reset.updateExpiry, data.uid) + ], callback); }); } diff --git a/src/user/reset.js b/src/user/reset.js index 9df72a8177..e9a5e46c1a 100644 --- a/src/user/reset.js +++ b/src/user/reset.js @@ -32,8 +32,17 @@ var async = require('async'), ], callback); }; + UserReset.generate = function(uid, callback) { + var code = utils.generateUUID(); + async.parallel([ + async.apply(db.setObjectField, 'reset:uid', code, uid), + async.apply(db.sortedSetAdd, 'reset:issueDate', Date.now(), code) + ], function(err) { + callback(err, code); + }); + }; + UserReset.send = function(email, callback) { - var reset_code = utils.generateUUID(); var uid; async.waterfall([ function(next) { @@ -45,18 +54,15 @@ var async = require('async'), } uid = _uid; - async.parallel([ - async.apply(db.setObjectField, 'reset:uid', reset_code, uid), - async.apply(db.sortedSetAdd, 'reset:issueDate', Date.now(), reset_code) - ], next); + UserReset.generate(uid, next); }, - function(results, next) { + function(code, next) { translator.translate('[[email:password-reset-requested, ' + (meta.config.title || 'NodeBB') + ']]', meta.config.defaultLang, function(subject) { - next(null, subject); + next(null, subject, code); }); }, - function(subject, next) { - var reset_link = nconf.get('url') + '/reset/' + reset_code; + function(subject, code, next) { + var reset_link = nconf.get('url') + '/reset/' + code; emailer.send('reset', uid, { site_title: (meta.config.title || 'NodeBB'), reset_link: reset_link, @@ -64,9 +70,6 @@ var async = require('async'), template: 'reset', uid: uid }, next); - }, - function(next) { - next(null, reset_code); } ], callback); }; @@ -96,12 +99,22 @@ var async = require('async'), async.apply(user.setUserField, uid, 'password', hash), async.apply(db.deleteObjectField, 'reset:uid', code), async.apply(db.sortedSetRemove, 'reset:issueDate', code), + async.apply(user.reset.updateExpiry, uid), async.apply(user.auth.resetLockout, uid) ], next); } ], callback); }; + UserReset.updateExpiry = function(uid, callback) { + var oneDay = 1000*60*60*24, + expireDays = parseInt(meta.config.passwordExpiryDays || 0, 10), + expiry = Date.now() + (oneDay * expireDays); + + callback = callback || function() {}; + user.setUserField(uid, 'passwordExpiry', expireDays > 0 ? expiry : 0, callback); + }; + UserReset.clean = function(callback) { async.waterfall([ async.apply(db.getSortedSetRangeByScore, 'reset:issueDate', 0, -1, 0, Date.now() - twoHours), diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index c0c1ca8ab3..ab09e237c8 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -102,6 +102,10 @@ +
+ + +