Ghost/core/server/lib/security/tokens.js

116 lines
3.0 KiB
JavaScript
Raw Normal View History

const crypto = require('crypto');
module.exports.generateFromContent = function generateFromContent(options) {
options = options || {};
const hash = crypto.createHash('sha256'),
content = options.content;
let text = '';
hash.update(content);
text += [content, hash.digest('base64')].join('|');
return new Buffer(text).toString('base64');
};
module.exports.generateFromEmail = function generateFromEmail(options) {
options = options || {};
const hash = crypto.createHash('sha256'),
expires = options.expires,
email = options.email,
secret = options.secret;
let text = '';
hash.update(String(expires));
hash.update(email.toLocaleLowerCase());
hash.update(String(secret));
text += [expires, email, hash.digest('base64')].join('|');
return new Buffer(text).toString('base64');
};
module.exports.resetToken = {
generateHash: function generateHash(options) {
options = options || {};
var hash = crypto.createHash('sha256'),
expires = options.expires,
email = options.email,
dbHash = options.dbHash,
password = options.password,
text = '';
🎨 remove token logic from user model (#7622) * 🔥 remove User model functions - validateToken - generateToken - resetPassword - all this logic will re-appear in a different way Token logic: - was already extracted as separate PR, see https://github.com/TryGhost/Ghost/pull/7554 - we will use this logic in the controller, you will see in the next commits Reset Password: Was just a wrapper for calling the token logic and change the password. We can reconsider keeping the function to call: changePassword and activate the status of the user - but i think it's fine to trigger these two actions from the controlling unit. * 🔥 remove password reset tests from User model - we already have unit tests for change password and the token logic - i will re-check at the end if any test case is missing - but for now i will just burn the tests * ✨ add token logic to controlling unit generateResetToken endpoint - the only change here is instead of calling the User model to generate a token, we generate the token via utils - we fetch the user by email, and generate a hash and return resetPassword endpoint - here we have changed a little bit more - first of all: we have added the validation check if the new passwords match - a new helper method to extract the token informations - the brute force security check, which can be handled later from the new bruteforce middleware (see TODO) - the actual reset function is doing the steps: load me the user, compare the token, change the password and activate the user - we can think of wrapping these steps into a User model function - i was not sure about it, because it is actually part of the controlling unit [ci skip] * 🎨 tidy up - jscs - jshint - naming functions - fixes * ✨ add a test for resetting the password - there was none - added a test to reset the password * 🎨 add more token tests - ensure quality - ensure logic we had * 🔥 remove compare new password check from User Model - this part of controlling unit * ✨ compare new passwords for user endpoint - we deleted the logic in User Model - we are adding the logic to controlling unit * 🐛 spam prevention forgotten can crash - no validation happend before this middleware - it just assumes that the root key is present - when we work on our API, we need to ensure that 1. pre validation happens 2. we call middlewares 3. ... * 🎨 token translation key
2016-11-07 14:18:50 +03:00
hash.update(String(expires));
hash.update(email.toLocaleLowerCase());
hash.update(password);
hash.update(String(dbHash));
text += [expires, email, hash.digest('base64')].join('|');
return new Buffer(text).toString('base64');
},
extract: function extract(options) {
options = options || {};
var token = options.token,
tokenText = new Buffer(token, 'base64').toString('ascii'),
parts,
expires,
email;
parts = tokenText.split('|');
// Check if invalid structure
if (!parts || parts.length !== 3) {
return false;
}
expires = parseInt(parts[0], 10);
email = parts[1];
return {
expires: expires,
email: email
};
},
compare: function compare(options) {
options = options || {};
var tokenToCompare = options.token,
parts = exports.resetToken.extract({token: tokenToCompare}),
dbHash = options.dbHash,
password = options.password,
generatedToken,
diff = 0,
i;
if (isNaN(parts.expires)) {
return false;
}
// Check if token is expired to prevent replay attacks
if (parts.expires < Date.now()) {
return false;
}
generatedToken = exports.resetToken.generateHash({
email: parts.email,
expires: parts.expires,
dbHash: dbHash,
password: password
});
if (tokenToCompare.length !== generatedToken.length) {
diff = 1;
}
for (i = tokenToCompare.length - 1; i >= 0; i = i - 1) {
diff |= tokenToCompare.charCodeAt(i) ^ generatedToken.charCodeAt(i);
}
return diff === 0;
}
};