mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 03:14:03 +03:00
Merge pull request #1534 from jgable/passwordReset
Improved Password Reset Tool
This commit is contained in:
commit
2701f3e664
@ -15,7 +15,8 @@
|
||||
|
||||
.ghost-login,
|
||||
.ghost-signup,
|
||||
.ghost-forgotten {
|
||||
.ghost-forgotten,
|
||||
.ghost-reset {
|
||||
color: $midgrey;
|
||||
background: $darkgrey;
|
||||
|
||||
@ -35,7 +36,8 @@
|
||||
|
||||
.login-box,
|
||||
.signup-box,
|
||||
.forgotten-box {
|
||||
.forgotten-box,
|
||||
.reset-box {
|
||||
max-width: 530px;
|
||||
height: 90%;
|
||||
margin: 0 auto;
|
||||
@ -177,10 +179,10 @@
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
2. Signup
|
||||
2. Signup and Reset
|
||||
============================================================================= */
|
||||
|
||||
#signup {
|
||||
#signup, #reset {
|
||||
@include box-sizing(border-box);
|
||||
max-width: 280px;
|
||||
color: lighten($midgrey, 15%);
|
||||
|
@ -13,7 +13,8 @@
|
||||
'register/' : 'register',
|
||||
'signup/' : 'signup',
|
||||
'signin/' : 'login',
|
||||
'forgotten/' : 'forgotten'
|
||||
'forgotten/' : 'forgotten',
|
||||
'reset/:token/' : 'reset'
|
||||
},
|
||||
|
||||
signup: function () {
|
||||
@ -28,6 +29,10 @@
|
||||
Ghost.currentView = new Ghost.Views.Forgotten({ el: '.js-forgotten-box' });
|
||||
},
|
||||
|
||||
reset: function (token) {
|
||||
Ghost.currentView = new Ghost.Views.ResetPassword({ el: '.js-reset-box', token: token });
|
||||
},
|
||||
|
||||
blog: function () {
|
||||
var posts = new Ghost.Collections.Posts();
|
||||
NProgress.start();
|
||||
|
9
core/client/tpl/reset.hbs
Normal file
9
core/client/tpl/reset.hbs
Normal file
@ -0,0 +1,9 @@
|
||||
<form id="reset" method="post" novalidate="novalidate">
|
||||
<div class="password-wrap">
|
||||
<input class="password" type="password" placeholder="Password" name="newpassword" />
|
||||
</div>
|
||||
<div class="password-wrap">
|
||||
<input class="password" type="password" placeholder="Confirm Password" name="ne2password" />
|
||||
</div>
|
||||
<button class="button-save" type="submit">Reset Password</button>
|
||||
</form>
|
@ -6,9 +6,6 @@
|
||||
|
||||
initialize: function () {
|
||||
this.render();
|
||||
$(".js-login-box").css({"opacity": 0}).animate({"opacity": 1}, 500, function () {
|
||||
$("[name='email']").focus();
|
||||
});
|
||||
},
|
||||
|
||||
templateName: "login",
|
||||
@ -17,6 +14,13 @@
|
||||
'submit #login': 'submitHandler'
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
var self = this;
|
||||
this.$el.css({"opacity": 0}).animate({"opacity": 1}, 500, function () {
|
||||
self.$("[name='email']").focus();
|
||||
});
|
||||
},
|
||||
|
||||
submitHandler: function (event) {
|
||||
event.preventDefault();
|
||||
var email = this.$el.find('.email').val(),
|
||||
@ -61,9 +65,6 @@
|
||||
|
||||
initialize: function () {
|
||||
this.render();
|
||||
$(".js-signup-box").css({"opacity": 0}).animate({"opacity": 1}, 500, function () {
|
||||
$("[name='name']").focus();
|
||||
});
|
||||
},
|
||||
|
||||
templateName: "signup",
|
||||
@ -72,11 +73,21 @@
|
||||
'submit #signup': 'submitHandler'
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
var self = this;
|
||||
|
||||
this.$el
|
||||
.css({"opacity": 0})
|
||||
.animate({"opacity": 1}, 500, function () {
|
||||
self.$("[name='name']").focus();
|
||||
});
|
||||
},
|
||||
|
||||
submitHandler: function (event) {
|
||||
event.preventDefault();
|
||||
var name = this.$el.find('.name').val(),
|
||||
email = this.$el.find('.email').val(),
|
||||
password = this.$el.find('.password').val();
|
||||
var name = this.$('.name').val(),
|
||||
email = this.$('.email').val(),
|
||||
password = this.$('.password').val();
|
||||
|
||||
// This is needed due to how error handling is done. If this is not here, there will not be a time
|
||||
// when there is no error.
|
||||
@ -119,9 +130,6 @@
|
||||
|
||||
initialize: function () {
|
||||
this.render();
|
||||
$(".js-forgotten-box").css({"opacity": 0}).animate({"opacity": 1}, 500, function () {
|
||||
$("[name='email']").focus();
|
||||
});
|
||||
},
|
||||
|
||||
templateName: "forgotten",
|
||||
@ -130,6 +138,13 @@
|
||||
'submit #forgotten': 'submitHandler'
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
var self = this;
|
||||
this.$el.css({"opacity": 0}).animate({"opacity": 1}, 500, function () {
|
||||
self.$("[name='email']").focus();
|
||||
});
|
||||
},
|
||||
|
||||
submitHandler: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
@ -166,4 +181,74 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ghost.Views.ResetPassword = Ghost.View.extend({
|
||||
templateName: 'reset',
|
||||
|
||||
events: {
|
||||
'submit #reset': 'submitHandler'
|
||||
},
|
||||
|
||||
initialize: function (attrs) {
|
||||
attrs = attrs || {};
|
||||
|
||||
this.token = attrs.token;
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
afterRender: function () {
|
||||
var self = this;
|
||||
this.$el.css({"opacity": 0}).animate({"opacity": 1}, 500, function () {
|
||||
self.$("[name='newpassword']").focus();
|
||||
});
|
||||
},
|
||||
|
||||
submitHandler: function (ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
var self = this,
|
||||
newPassword = this.$('input[name="newpassword"]').val(),
|
||||
ne2Password = this.$('input[name="ne2password"]').val();
|
||||
|
||||
if (newPassword !== ne2Password) {
|
||||
Ghost.notifications.addItem({
|
||||
type: 'error',
|
||||
message: "Your passwords do not match.",
|
||||
status: 'passive'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$('input, button').prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url: '/ghost/reset/' + this.token + '/',
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||
},
|
||||
data: {
|
||||
newpassword: newPassword,
|
||||
ne2password: ne2Password
|
||||
},
|
||||
success: function (msg) {
|
||||
window.location.href = msg.redirect;
|
||||
},
|
||||
error: function (xhr) {
|
||||
self.$('input, button').prop('disabled', false);
|
||||
|
||||
Ghost.notifications.clearEverything();
|
||||
Ghost.notifications.addItem({
|
||||
type: 'error',
|
||||
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),
|
||||
status: 'passive'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}());
|
||||
|
@ -20,7 +20,8 @@ var Ghost = require('../../ghost'),
|
||||
settingsObject,
|
||||
settingsCollection,
|
||||
settingsFilter,
|
||||
filteredUserAttributes = ['password', 'created_by', 'updated_by', 'last_login'];
|
||||
filteredUserAttributes = ['password', 'created_by', 'updated_by', 'last_login'],
|
||||
ONE_DAY = 86400000;
|
||||
|
||||
// ## Posts
|
||||
posts = {
|
||||
@ -218,8 +219,19 @@ users = {
|
||||
return dataProvider.User.changePassword(userData);
|
||||
},
|
||||
|
||||
forgottenPassword: function forgottenPassword(email) {
|
||||
return dataProvider.User.forgottenPassword(email);
|
||||
generateResetToken: function generateResetToken(email) {
|
||||
// TODO: Do we want to be able to pass this in?
|
||||
var expires = Date.now() + ONE_DAY;
|
||||
|
||||
return dataProvider.User.generateResetToken(email, expires, ghost.dbHash);
|
||||
},
|
||||
|
||||
validateToken: function validateToken(token) {
|
||||
return dataProvider.User.validateToken(token, ghost.dbHash);
|
||||
},
|
||||
|
||||
resetPassword: function resetPassword(token, newPassword, ne2Password) {
|
||||
return dataProvider.User.resetPassword(token, newPassword, ne2Password, ghost.dbHash);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
var Ghost = require('../../ghost'),
|
||||
_ = require('underscore'),
|
||||
path = require('path'),
|
||||
when = require('when'),
|
||||
api = require('../api'),
|
||||
errors = require('../errorHandling'),
|
||||
storage = require('../storage'),
|
||||
@ -94,7 +95,7 @@ adminControllers = {
|
||||
}
|
||||
},
|
||||
'changepw': function (req, res) {
|
||||
api.users.changePassword({
|
||||
return api.users.changePassword({
|
||||
currentUser: req.session.user,
|
||||
oldpw: req.body.password,
|
||||
newpw: req.body.newpassword,
|
||||
@ -104,7 +105,6 @@ adminControllers = {
|
||||
}, function (error) {
|
||||
res.send(401, {error: error.message});
|
||||
});
|
||||
|
||||
},
|
||||
'signup': function (req, res) {
|
||||
/*jslint unparam:true*/
|
||||
@ -114,7 +114,6 @@ adminControllers = {
|
||||
adminNav: setSelected(adminNavbar, 'login')
|
||||
});
|
||||
},
|
||||
|
||||
'doRegister': function (req, res) {
|
||||
var name = req.body.name,
|
||||
email = req.body.email,
|
||||
@ -134,9 +133,7 @@ adminControllers = {
|
||||
}).otherwise(function (error) {
|
||||
res.json(401, {error: error.message});
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
'forgotten': function (req, res) {
|
||||
/*jslint unparam:true*/
|
||||
res.render('forgotten', {
|
||||
@ -145,26 +142,27 @@ adminControllers = {
|
||||
adminNav: setSelected(adminNavbar, 'login')
|
||||
});
|
||||
},
|
||||
|
||||
'resetPassword': function (req, res) {
|
||||
'generateResetToken': function (req, res) {
|
||||
var email = req.body.email;
|
||||
|
||||
api.users.forgottenPassword(email).then(function (user) {
|
||||
var message = {
|
||||
api.users.generateResetToken(email).then(function (token) {
|
||||
var siteLink = '<a href="' + ghost.config().url + '">' + ghost.config().url + '</a>',
|
||||
resetUrl = ghost.config().url + '/ghost/reset/' + token + '/',
|
||||
resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
|
||||
message = {
|
||||
to: email,
|
||||
subject: 'Your new password',
|
||||
html: "<p><strong>Hello!</strong></p>" +
|
||||
"<p>You've reset your password. Here's the new one: " + user.newPassword + "</p>" +
|
||||
"<p>Ghost <br/>" +
|
||||
'<a href="' + ghost.config().url + '">' +
|
||||
ghost.config().url + '</a></p>'
|
||||
subject: 'Reset Password',
|
||||
html: '<p><strong>Hello!</strong></p>' +
|
||||
'<p>A request has been made to reset the password on the site ' + siteLink + '.</p>' +
|
||||
'<p>Please follow the link below to reset your password:<br><br>' + resetLink + '</p>' +
|
||||
'<p>Ghost</p>'
|
||||
};
|
||||
|
||||
return ghost.mail.send(message);
|
||||
}).then(function success() {
|
||||
var notification = {
|
||||
type: 'success',
|
||||
message: 'Your password was changed successfully. Check your email for details.',
|
||||
message: 'Check your email for further instructions',
|
||||
status: 'passive',
|
||||
id: 'successresetpw'
|
||||
};
|
||||
@ -174,8 +172,62 @@ adminControllers = {
|
||||
});
|
||||
|
||||
}, function failure(error) {
|
||||
// TODO: This is kind of sketchy, depends on magic string error.message from Bookshelf.
|
||||
// TODO: It's debatable whether we want to just tell the user we sent the email in this case or not, we are giving away sensitive info here.
|
||||
if (error && error.message === 'EmptyResponse') {
|
||||
error.message = "Invalid email address";
|
||||
}
|
||||
|
||||
res.json(401, {error: error.message});
|
||||
}).otherwise(errors.logAndThrowError);
|
||||
});
|
||||
},
|
||||
'reset': function (req, res) {
|
||||
// Validate the request token
|
||||
var token = req.params.token;
|
||||
|
||||
api.users.validateToken(token).then(function () {
|
||||
// Render the reset form
|
||||
res.render('reset', {
|
||||
bodyClass: 'ghost-reset',
|
||||
hideNavbar: true,
|
||||
adminNav: setSelected(adminNavbar, 'reset')
|
||||
});
|
||||
}).otherwise(function (err) {
|
||||
// Redirect to forgotten if invalid token
|
||||
var notification = {
|
||||
type: 'error',
|
||||
message: 'Invalid or expired token',
|
||||
status: 'persistent',
|
||||
id: 'errorinvalidtoken'
|
||||
};
|
||||
|
||||
errors.logError(err, 'admin.js', "Please check the provided token for validity and expiration.");
|
||||
|
||||
return api.notifications.add(notification).then(function () {
|
||||
res.redirect('/ghost/forgotten');
|
||||
});
|
||||
});
|
||||
},
|
||||
'resetPassword': function (req, res) {
|
||||
var token = req.params.token,
|
||||
newPassword = req.param('newpassword'),
|
||||
ne2Password = req.param('ne2password');
|
||||
|
||||
api.users.resetPassword(token, newPassword, ne2Password).then(function () {
|
||||
var notification = {
|
||||
type: 'success',
|
||||
message: 'Password changed successfully.',
|
||||
status: 'passive',
|
||||
id: 'successresetpw'
|
||||
};
|
||||
|
||||
return api.notifications.add(notification).then(function () {
|
||||
res.json(200, {redirect: '/ghost/signin/'});
|
||||
});
|
||||
}).otherwise(function (err) {
|
||||
// TODO: Better error message if we can tell whether the passwords didn't match or something
|
||||
res.json(401, {error: err.message});
|
||||
});
|
||||
},
|
||||
'logout': function (req, res) {
|
||||
req.session = null;
|
||||
|
@ -13,10 +13,9 @@ var User,
|
||||
http = require('http'),
|
||||
crypto = require('crypto');
|
||||
|
||||
|
||||
function validatePasswordLength(password) {
|
||||
try {
|
||||
ghostBookshelf.validator.check(password, "Your must be at least 8 characters long.").len(8);
|
||||
ghostBookshelf.validator.check(password, "Your password must be at least 8 characters long.").len(8);
|
||||
} catch (error) {
|
||||
return when.reject(error);
|
||||
}
|
||||
@ -24,6 +23,14 @@ function validatePasswordLength(password) {
|
||||
return when.resolve();
|
||||
}
|
||||
|
||||
function generatePasswordHash(password) {
|
||||
// Generate a new salt
|
||||
return nodefn.call(bcrypt.genSalt).then(function (salt) {
|
||||
// Hash the provided password with bcrypt
|
||||
return nodefn.call(bcrypt.hash, password, salt);
|
||||
});
|
||||
}
|
||||
|
||||
User = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'users',
|
||||
@ -107,11 +114,8 @@ User = ghostBookshelf.Model.extend({
|
||||
return when.reject(new Error('A user is already registered. Only one user for now!'));
|
||||
}
|
||||
}).then(function () {
|
||||
// Generate a new salt
|
||||
return nodefn.call(bcrypt.genSalt);
|
||||
}).then(function (salt) {
|
||||
// Hash the provided password with bcrypt
|
||||
return nodefn.call(bcrypt.hash, _user.password, salt);
|
||||
// Generate a new password hash
|
||||
return generatePasswordHash(_user.password);
|
||||
}).then(function (hash) {
|
||||
// Assign the hashed password
|
||||
userData.password = hash;
|
||||
@ -205,21 +209,85 @@ User = ghostBookshelf.Model.extend({
|
||||
});
|
||||
},
|
||||
|
||||
forgottenPassword: function (email) {
|
||||
var newPassword = Math.random().toString(36).slice(2, 12), // This is magick
|
||||
user = null;
|
||||
generateResetToken: function (email, expires, dbHash) {
|
||||
return this.forge({email: email.toLocaleLowerCase()}).fetch({require: true}).then(function (foundUser) {
|
||||
var hash = crypto.createHash('sha256'),
|
||||
text = "";
|
||||
|
||||
return this.forge({email: email}).fetch({require: true}).then(function (_user) {
|
||||
user = _user;
|
||||
return nodefn.call(bcrypt.genSalt);
|
||||
}).then(function (salt) {
|
||||
return nodefn.call(bcrypt.hash, newPassword, salt);
|
||||
}).then(function (hash) {
|
||||
user.save({password: hash});
|
||||
return { user: user, newPassword: newPassword };
|
||||
}, function (error) {
|
||||
/*jslint unparam:true*/
|
||||
return when.reject(new Error('There is no user by that email address. Check again.'));
|
||||
// Token:
|
||||
// BASE64(TIMESTAMP + email + HASH(TIMESTAMP + email + oldPasswordHash + dbHash ))
|
||||
|
||||
hash.update(String(expires));
|
||||
hash.update(email.toLocaleLowerCase());
|
||||
hash.update(foundUser.get('password'));
|
||||
hash.update(String(dbHash));
|
||||
|
||||
text += [expires, email, hash.digest('base64')].join('|');
|
||||
|
||||
return new Buffer(text).toString('base64');
|
||||
});
|
||||
},
|
||||
|
||||
validateToken: function (token, dbHash) {
|
||||
// TODO: Is there a chance the use of ascii here will cause problems if oldPassword has weird characters?
|
||||
var tokenText = new Buffer(token, 'base64').toString('ascii'),
|
||||
parts,
|
||||
expires,
|
||||
email;
|
||||
|
||||
parts = tokenText.split('|');
|
||||
|
||||
// Check if invalid structure
|
||||
if (!parts || parts.length !== 3) {
|
||||
return when.reject(new Error("Invalid token structure"));
|
||||
}
|
||||
|
||||
expires = parseInt(parts[0], 10);
|
||||
email = parts[1];
|
||||
|
||||
if (isNaN(expires)) {
|
||||
return when.reject(new Error("Invalid token expiration"));
|
||||
}
|
||||
|
||||
// This is easy to fake, but still check anyway.
|
||||
if (expires < Date.now()) {
|
||||
return when.reject(new Error("Expired token"));
|
||||
}
|
||||
|
||||
return this.generateResetToken(email, expires, dbHash).then(function (generatedToken) {
|
||||
// Check for matching tokens
|
||||
if (token === generatedToken) {
|
||||
return when.resolve(email);
|
||||
}
|
||||
|
||||
return when.reject(new Error("Invalid token"));
|
||||
});
|
||||
},
|
||||
|
||||
resetPassword: function (token, newPassword, ne2Password, dbHash) {
|
||||
var self = this;
|
||||
|
||||
if (newPassword !== ne2Password) {
|
||||
return when.reject(new Error("Your new passwords do not match"));
|
||||
}
|
||||
|
||||
return validatePasswordLength(newPassword).then(function () {
|
||||
// Validate the token; returns the email address from token
|
||||
return self.validateToken(token, dbHash);
|
||||
}).then(function (email) {
|
||||
// Fetch the user by email, and hash the password at the same time.
|
||||
return when.join(
|
||||
self.forge({email: email.toLocaleLowerCase()}).fetch({require: true}),
|
||||
generatePasswordHash(newPassword)
|
||||
);
|
||||
}).then(function (results) {
|
||||
// Update the user with the new password hash
|
||||
var foundUser = results[0],
|
||||
passwordHash = results[1];
|
||||
|
||||
foundUser.save({password: passwordHash});
|
||||
|
||||
return foundUser;
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -30,7 +30,9 @@ module.exports = function (server) {
|
||||
server.get('/ghost/signin/', redirectToSignup, middleware.redirectToDashboard, admin.login);
|
||||
server.get('/ghost/signup/', middleware.redirectToDashboard, admin.signup);
|
||||
server.get('/ghost/forgotten/', middleware.redirectToDashboard, admin.forgotten);
|
||||
server.post('/ghost/forgotten/', admin.resetPassword);
|
||||
server.post('/ghost/forgotten/', admin.generateResetToken);
|
||||
server.get('/ghost/reset/:token', admin.reset);
|
||||
server.post('/ghost/reset/:token', admin.resetPassword);
|
||||
server.post('/ghost/signin/', admin.auth);
|
||||
server.post('/ghost/signup/', admin.doRegister);
|
||||
server.post('/ghost/changepw/', middleware.auth, admin.changepw);
|
||||
|
4
core/server/views/reset.hbs
Normal file
4
core/server/views/reset.hbs
Normal file
@ -0,0 +1,4 @@
|
||||
{{!< default}}
|
||||
<section class="reset-box js-reset-box">
|
||||
|
||||
</section>
|
@ -5,6 +5,7 @@ var testUtils = require('../../utils'),
|
||||
_ = require('underscore'),
|
||||
errors = require('../../../server/errorHandling'),
|
||||
sinon = require('sinon'),
|
||||
uuid = require('node-uuid'),
|
||||
|
||||
// Stuff we are testing
|
||||
Models = require('../../../server/models');
|
||||
@ -222,6 +223,136 @@ describe('User Model', function run() {
|
||||
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
it('can generate reset token', function (done) {
|
||||
// Expires in one minute
|
||||
var expires = Date.now() + 60000,
|
||||
dbHash = uuid.v4();
|
||||
|
||||
UserModel.browse().then(function (results) {
|
||||
|
||||
return UserModel.generateResetToken(results.models[0].attributes.email, expires, dbHash);
|
||||
|
||||
}).then(function (token) {
|
||||
should.exist(token);
|
||||
|
||||
token.length.should.be.above(0);
|
||||
|
||||
done();
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
it('can validate a reset token', function (done) {
|
||||
// Expires in one minute
|
||||
var expires = Date.now() + 60000,
|
||||
dbHash = uuid.v4();
|
||||
|
||||
UserModel.browse().then(function (results) {
|
||||
|
||||
return UserModel.generateResetToken(results.models[0].attributes.email, expires, dbHash);
|
||||
|
||||
}).then(function (token) {
|
||||
|
||||
return UserModel.validateToken(token, dbHash);
|
||||
|
||||
}).then(function () {
|
||||
|
||||
done();
|
||||
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
it('can reset a password with a valid token', function (done) {
|
||||
// Expires in one minute
|
||||
var origPassword,
|
||||
expires = Date.now() + 60000,
|
||||
dbHash = uuid.v4();
|
||||
|
||||
UserModel.browse().then(function (results) {
|
||||
|
||||
var firstUser = results.models[0],
|
||||
origPassword = firstUser.attributes.password;
|
||||
|
||||
should.exist(origPassword);
|
||||
|
||||
return UserModel.generateResetToken(firstUser.attributes.email, expires, dbHash);
|
||||
|
||||
}).then(function (token) {
|
||||
|
||||
return UserModel.resetPassword(token, 'newpassword', 'newpassword', dbHash);
|
||||
|
||||
}).then(function (resetUser) {
|
||||
var resetPassword = resetUser.get('password');
|
||||
|
||||
should.exist(resetPassword);
|
||||
|
||||
resetPassword.should.not.equal(origPassword);
|
||||
|
||||
done();
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
it('doesn\'t allow expired timestamp tokens', function (done) {
|
||||
var email,
|
||||
// Expired one minute ago
|
||||
expires = Date.now() - 60000,
|
||||
dbHash = uuid.v4();
|
||||
|
||||
UserModel.browse().then(function (results) {
|
||||
|
||||
// Store email for later
|
||||
email = results.models[0].attributes.email;
|
||||
|
||||
return UserModel.generateResetToken(email, expires, dbHash);
|
||||
|
||||
}).then(function (token) {
|
||||
return UserModel.validateToken(token, dbHash);
|
||||
}).then(function () {
|
||||
throw new Error("Allowed expired token");
|
||||
}, function (err) {
|
||||
|
||||
should.exist(err);
|
||||
|
||||
err.message.should.equal("Expired token");
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('doesn\'t allow tampered timestamp tokens', function (done) {
|
||||
// Expired one minute ago
|
||||
var expires = Date.now() - 60000,
|
||||
dbHash = uuid.v4();
|
||||
|
||||
UserModel.browse().then(function (results) {
|
||||
|
||||
return UserModel.generateResetToken(results.models[0].attributes.email, expires, dbHash);
|
||||
|
||||
}).then(function (token) {
|
||||
|
||||
var tokenText = new Buffer(token, 'base64').toString('ascii'),
|
||||
parts = tokenText.split('|'),
|
||||
fakeExpires,
|
||||
fakeToken;
|
||||
|
||||
fakeExpires = Date.now() + 60000;
|
||||
|
||||
fakeToken = [String(fakeExpires), parts[1], parts[2]].join('|');
|
||||
fakeToken = new Buffer(fakeToken).toString('base64');
|
||||
|
||||
return UserModel.validateToken(fakeToken, dbHash);
|
||||
|
||||
}).then(function () {
|
||||
throw new Error("allowed invalid token");
|
||||
}, function (err) {
|
||||
|
||||
should.exist(err);
|
||||
|
||||
err.message.should.equal("Invalid token");
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user