0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-03 23:00:14 -05:00

URL safe base64 encoding

closes #3872
- updated base64 escaping to respect + and \
- updated base64 escaping to remove = during transport
- updated tests
This commit is contained in:
Sebastian Gierlinger 2014-12-01 16:59:49 +01:00
parent 3322fd6ff8
commit 9ddabffa10
5 changed files with 41 additions and 15 deletions

View file

@ -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) {

View file

@ -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) {

View file

@ -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(

View file

@ -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;
}
};

View file

@ -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');