diff --git a/core/server/api/authentication.js b/core/server/api/authentication.js index 2b54ce0382..29cef832a5 100644 --- a/core/server/api/authentication.js +++ b/core/server/api/authentication.js @@ -47,7 +47,7 @@ authentication = { return dataProvider.User.generateResetToken(email, expires, dbHash); }).then(function (resetToken) { var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url, - resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + resetToken + '/'; + resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + globalUtils.encodeBase64URLsafe(resetToken) + '/'; return mail.generateContent({data: {resetUrl: resetUrl}, template: 'reset-password'}); }).then(function (emailContent) { diff --git a/core/server/api/users.js b/core/server/api/users.js index 0c20703be2..0bc8fe9ae7 100644 --- a/core/server/api/users.js +++ b/core/server/api/users.js @@ -46,7 +46,7 @@ sendInviteEmail = function sendInviteEmail(user) { }).then(function (resetToken) { var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url; - emailData.resetLink = baseUrl.replace(/\/$/, '') + '/ghost/signup/' + resetToken + '/'; + emailData.resetLink = baseUrl.replace(/\/$/, '') + '/ghost/signup/' + globalUtils.encodeBase64URLsafe(resetToken) + '/'; return mail.generateContent({data: emailData, template: 'invite-user'}); }).then(function (emailContent) { diff --git a/core/server/models/user.js b/core/server/models/user.js index 444f8e500a..6333013173 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -1,6 +1,7 @@ var _ = require('lodash'), Promise = require('bluebird'), errors = require('../errors'), + utils = require('../utils'), bcrypt = require('bcryptjs'), ghostBookshelf = require('./base'), crypto = require('crypto'), @@ -706,28 +707,20 @@ User = ghostBookshelf.Model.extend({ text = ''; // Token: - // BASE64(TIMESTAMP + email + HASH(TIMESTAMP + email + oldPasswordHash + dbHash )).replace('=', '-') - + // 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('|'); - - // it's possible that the token might get URI encoded, which breaks it - // we replace any `=`s with `-`s as they aren't valid base64 characters - // but are valid in a URL, so won't suffer encoding issues - return new Buffer(text).toString('base64').replace('=', '-'); + return new Buffer(text).toString('base64'); }); }, validateToken: function (token, dbHash) { /*jslint bitwise:true*/ // TODO: Is there a chance the use of ascii here will cause problems if oldPassword has weird characters? - // We replaced `=`s with `-`s when we sent the token via email, so - // now we reverse that change to get a valid base64 string to decode - token = token.replace('-', '='); var tokenText = new Buffer(token, 'base64').toString('ascii'), parts, expires, @@ -793,7 +786,7 @@ User = ghostBookshelf.Model.extend({ return validatePasswordLength(newPassword).then(function () { // Validate the token; returns the email address from token - return self.validateToken(token, dbHash); + return self.validateToken(utils.decodeBase64URLsafe(token), dbHash); }).then(function (email) { // Fetch the user by email, and hash the password at the same time. return Promise.join( diff --git a/core/server/utils/index.js b/core/server/utils/index.js index 45c0f06552..b95f8df379 100644 --- a/core/server/utils/index.js +++ b/core/server/utils/index.js @@ -64,6 +64,19 @@ utils = { .toLowerCase(); return string; + }, + // The token is encoded URL safe by replcaing '+' with '-', '\' with '_' and removing '=' + // NOTE: the token is not encoded using valid base64 anymore + encodeBase64URLsafe: function (base64String) { + return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + }, + // Decode url safe base64 encoding and add padding ('=') + decodeBase64URLsafe: function (base64String) { + base64String = base64String.replace(/-/g, '+').replace(/_/g, '/'); + while (base64String.length % 4) { + base64String += '='; + } + return base64String; } }; diff --git a/core/test/integration/model/model_users_spec.js b/core/test/integration/model/model_users_spec.js index 4a7f8c6da6..2e03bf871d 100644 --- a/core/test/integration/model/model_users_spec.js +++ b/core/test/integration/model/model_users_spec.js @@ -8,6 +8,7 @@ var testUtils = require('../../utils'), _ = require('lodash'), // Stuff we are testing + utils = require('../../../server/utils'), UserModel = require('../../../server/models/user').User, RoleModel = require('../../../server/models/role').Role, context = testUtils.context.admin, @@ -354,7 +355,7 @@ describe('User Model', function run() { }); describe('Password Reset', function () { - beforeEach(testUtils.setup('owner')); + beforeEach(testUtils.setup('users:roles')); it('can generate reset token', function (done) { // Expires in one minute @@ -378,7 +379,7 @@ describe('User Model', function run() { dbHash = uuid.v4(); UserModel.findAll().then(function (results) { - return UserModel.generateResetToken(results.models[0].attributes.email, expires, dbHash); + return UserModel.generateResetToken(results.models[1].attributes.email, expires, dbHash); }).then(function (token) { return UserModel.validateToken(token, dbHash); }).then(function () { @@ -386,6 +387,24 @@ describe('User Model', function run() { }).catch(done); }); + it('can validate an URI encoded reset token', function (done) { + // Expires in one minute + var expires = Date.now() + 60000, + dbHash = uuid.v4(); + + UserModel.findAll().then(function (results) { + return UserModel.generateResetToken(results.models[1].attributes.email, expires, dbHash); + }).then(function (token) { + token = utils.encodeBase64URLsafe(token); + token = encodeURIComponent(token); + token = decodeURIComponent(token); + token = utils.decodeBase64URLsafe(token); + return UserModel.validateToken(token, dbHash); + }).then(function () { + done(); + }).catch(done); + }); + it('can reset a password with a valid token', function (done) { // Expires in one minute var origPassword, @@ -400,6 +419,7 @@ describe('User Model', function run() { return UserModel.generateResetToken(firstUser.attributes.email, expires, dbHash); }).then(function (token) { + token = utils.encodeBase64URLsafe(token); return UserModel.resetPassword(token, 'newpassword', 'newpassword', dbHash); }).then(function (resetUser) { var resetPassword = resetUser.get('password');