mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Merge pull request #4554 from sebgie/issue#3872
URL safe base64 encoding
This commit is contained in:
commit
c06e649ab1
5 changed files with 41 additions and 15 deletions
|
@ -47,7 +47,7 @@ authentication = {
|
||||||
return dataProvider.User.generateResetToken(email, expires, dbHash);
|
return dataProvider.User.generateResetToken(email, expires, dbHash);
|
||||||
}).then(function (resetToken) {
|
}).then(function (resetToken) {
|
||||||
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
|
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'});
|
return mail.generateContent({data: {resetUrl: resetUrl}, template: 'reset-password'});
|
||||||
}).then(function (emailContent) {
|
}).then(function (emailContent) {
|
||||||
|
|
|
@ -46,7 +46,7 @@ sendInviteEmail = function sendInviteEmail(user) {
|
||||||
}).then(function (resetToken) {
|
}).then(function (resetToken) {
|
||||||
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url;
|
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'});
|
return mail.generateContent({data: emailData, template: 'invite-user'});
|
||||||
}).then(function (emailContent) {
|
}).then(function (emailContent) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
var _ = require('lodash'),
|
var _ = require('lodash'),
|
||||||
Promise = require('bluebird'),
|
Promise = require('bluebird'),
|
||||||
errors = require('../errors'),
|
errors = require('../errors'),
|
||||||
|
utils = require('../utils'),
|
||||||
bcrypt = require('bcryptjs'),
|
bcrypt = require('bcryptjs'),
|
||||||
ghostBookshelf = require('./base'),
|
ghostBookshelf = require('./base'),
|
||||||
crypto = require('crypto'),
|
crypto = require('crypto'),
|
||||||
|
@ -721,28 +722,20 @@ User = ghostBookshelf.Model.extend({
|
||||||
text = '';
|
text = '';
|
||||||
|
|
||||||
// Token:
|
// Token:
|
||||||
// BASE64(TIMESTAMP + email + HASH(TIMESTAMP + email + oldPasswordHash + dbHash )).replace('=', '-')
|
// BASE64(TIMESTAMP + email + HASH(TIMESTAMP + email + oldPasswordHash + dbHash ))
|
||||||
|
|
||||||
hash.update(String(expires));
|
hash.update(String(expires));
|
||||||
hash.update(email.toLocaleLowerCase());
|
hash.update(email.toLocaleLowerCase());
|
||||||
hash.update(foundUser.get('password'));
|
hash.update(foundUser.get('password'));
|
||||||
hash.update(String(dbHash));
|
hash.update(String(dbHash));
|
||||||
|
|
||||||
text += [expires, email, hash.digest('base64')].join('|');
|
text += [expires, email, hash.digest('base64')].join('|');
|
||||||
|
return new Buffer(text).toString('base64');
|
||||||
// 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('=', '-');
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
validateToken: function (token, dbHash) {
|
validateToken: function (token, dbHash) {
|
||||||
/*jslint bitwise:true*/
|
/*jslint bitwise:true*/
|
||||||
// TODO: Is there a chance the use of ascii here will cause problems if oldPassword has weird characters?
|
// 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'),
|
var tokenText = new Buffer(token, 'base64').toString('ascii'),
|
||||||
parts,
|
parts,
|
||||||
expires,
|
expires,
|
||||||
|
@ -808,7 +801,7 @@ User = ghostBookshelf.Model.extend({
|
||||||
|
|
||||||
return validatePasswordLength(newPassword).then(function () {
|
return validatePasswordLength(newPassword).then(function () {
|
||||||
// Validate the token; returns the email address from token
|
// Validate the token; returns the email address from token
|
||||||
return self.validateToken(token, dbHash);
|
return self.validateToken(utils.decodeBase64URLsafe(token), dbHash);
|
||||||
}).then(function (email) {
|
}).then(function (email) {
|
||||||
// Fetch the user by email, and hash the password at the same time.
|
// Fetch the user by email, and hash the password at the same time.
|
||||||
return Promise.join(
|
return Promise.join(
|
||||||
|
|
|
@ -64,6 +64,19 @@ utils = {
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
return string;
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ var testUtils = require('../../utils'),
|
||||||
_ = require('lodash'),
|
_ = require('lodash'),
|
||||||
|
|
||||||
// Stuff we are testing
|
// Stuff we are testing
|
||||||
|
utils = require('../../../server/utils'),
|
||||||
UserModel = require('../../../server/models/user').User,
|
UserModel = require('../../../server/models/user').User,
|
||||||
RoleModel = require('../../../server/models/role').Role,
|
RoleModel = require('../../../server/models/role').Role,
|
||||||
context = testUtils.context.admin,
|
context = testUtils.context.admin,
|
||||||
|
@ -354,7 +355,7 @@ describe('User Model', function run() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Password Reset', function () {
|
describe('Password Reset', function () {
|
||||||
beforeEach(testUtils.setup('owner'));
|
beforeEach(testUtils.setup('users:roles'));
|
||||||
|
|
||||||
it('can generate reset token', function (done) {
|
it('can generate reset token', function (done) {
|
||||||
// Expires in one minute
|
// Expires in one minute
|
||||||
|
@ -378,7 +379,7 @@ describe('User Model', function run() {
|
||||||
dbHash = uuid.v4();
|
dbHash = uuid.v4();
|
||||||
|
|
||||||
UserModel.findAll().then(function (results) {
|
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) {
|
}).then(function (token) {
|
||||||
return UserModel.validateToken(token, dbHash);
|
return UserModel.validateToken(token, dbHash);
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
|
@ -386,6 +387,24 @@ describe('User Model', function run() {
|
||||||
}).catch(done);
|
}).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) {
|
it('can reset a password with a valid token', function (done) {
|
||||||
// Expires in one minute
|
// Expires in one minute
|
||||||
var origPassword,
|
var origPassword,
|
||||||
|
@ -400,6 +419,7 @@ describe('User Model', function run() {
|
||||||
|
|
||||||
return UserModel.generateResetToken(firstUser.attributes.email, expires, dbHash);
|
return UserModel.generateResetToken(firstUser.attributes.email, expires, dbHash);
|
||||||
}).then(function (token) {
|
}).then(function (token) {
|
||||||
|
token = utils.encodeBase64URLsafe(token);
|
||||||
return UserModel.resetPassword(token, 'newpassword', 'newpassword', dbHash);
|
return UserModel.resetPassword(token, 'newpassword', 'newpassword', dbHash);
|
||||||
}).then(function (resetUser) {
|
}).then(function (resetUser) {
|
||||||
var resetPassword = resetUser.get('password');
|
var resetPassword = resetUser.get('password');
|
||||||
|
|
Loading…
Add table
Reference in a new issue