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

Improved error messaging for password reset process

refs #11878

- When password reset link is invalid previous messaging left the user
without clear information about why the reset failed and what they could do about it.
- Updated messaging around password reset tokens including detection of
when password token has invalid structure, has expired or has already
been used
This commit is contained in:
Nazar Gargol 2020-09-16 17:42:21 +12:00
parent 6dc8d91ace
commit 32b37d7ba8
5 changed files with 111 additions and 22 deletions

View file

@ -48,7 +48,9 @@ function extractTokenParts(options) {
if (!tokenParts) { if (!tokenParts) {
return Promise.reject(new errors.UnauthorizedError({ return Promise.reject(new errors.UnauthorizedError({
message: i18n.t('errors.api.common.invalidTokenStructure') message: i18n.t('errors.api.passwordReset.corruptedToken.message'),
context: i18n.t('errors.api.passwordReset.corruptedToken.context'),
help: i18n.t('errors.api.passwordReset.corruptedToken.help')
})); }));
} }
@ -86,16 +88,29 @@ function doReset(options, tokenParts, settingsAPI) {
throw new errors.NotFoundError({message: i18n.t('errors.api.users.userNotFound')}); throw new errors.NotFoundError({message: i18n.t('errors.api.users.userNotFound')});
} }
let tokenIsCorrect = security.tokens.resetToken.compare({ let compareResult = security.tokens.resetToken.compare({
token: resetToken, token: resetToken,
dbHash: dbHash, dbHash: dbHash,
password: user.get('password') password: user.get('password')
}); });
if (!tokenIsCorrect) { if (!compareResult.correct) {
return Promise.reject(new errors.BadRequestError({ let error;
message: i18n.t('errors.api.common.invalidTokenStructure') if (compareResult.reason === 'expired' || compareResult.reason === 'invalid_expiry') {
})); error = new errors.BadRequestError({
message: i18n.t('errors.api.passwordReset.expired.message'),
context: i18n.t('errors.api.passwordReset.expired.context'),
help: i18n.t('errors.api.passwordReset.expired.help')
});
} else {
error = new errors.BadRequestError({
message: i18n.t('errors.api.passwordReset.invalidToken.message'),
context: i18n.t('errors.api.passwordReset.invalidToken.context'),
help: i18n.t('errors.api.passwordReset.invalidToken.help')
});
}
return Promise.reject(error);
} }
return models.User.changePassword({ return models.User.changePassword({

View file

@ -302,7 +302,6 @@
}, },
"api": { "api": {
"common": { "common": {
"invalidTokenStructure": "Invalid token structure",
"notImplemented": "The server does not support the functionality required to fulfill the request." "notImplemented": "The server does not support the functionality required to fulfill the request."
}, },
"authentication": { "authentication": {
@ -318,6 +317,23 @@
"checkEmailConfigInstructions": "Please see {url} for instructions on configuring email.", "checkEmailConfigInstructions": "Please see {url} for instructions on configuring email.",
"notTheBlogOwner": "You are not the site owner." "notTheBlogOwner": "You are not the site owner."
}, },
"passwordReset": {
"expired": {
"message": "Cannot reset password.",
"context": "Password reset link expired.",
"help": "Request a new password reset via the login form."
},
"invalidToken": {
"message": "Cannot reset password.",
"context": "Password reset link has already been used.",
"help": "Request a new password reset via the login form."
},
"corruptedToken": {
"message": "Cannot reset password.",
"context": "Invalid password reset link.",
"help": "Check if password reset link has been fully copied or request new password reset via the login form."
}
},
"configuration": { "configuration": {
"invalidKey": "Invalid key" "invalidKey": "Invalid key"
}, },

View file

@ -60,7 +60,7 @@
"@tryghost/members-ssr": "0.8.5", "@tryghost/members-ssr": "0.8.5",
"@tryghost/mw-session-from-token": "0.1.7", "@tryghost/mw-session-from-token": "0.1.7",
"@tryghost/promise": "0.1.0", "@tryghost/promise": "0.1.0",
"@tryghost/security": "0.1.0", "@tryghost/security": "0.2.0",
"@tryghost/session-service": "0.1.8", "@tryghost/session-service": "0.1.8",
"@tryghost/social-urls": "0.1.12", "@tryghost/social-urls": "0.1.12",
"@tryghost/string": "0.1.11", "@tryghost/string": "0.1.11",

View file

@ -305,7 +305,72 @@ describe('Authentication API v3', function () {
.then((res) => { .then((res) => {
should.exist(res.body.errors); should.exist(res.body.errors);
res.body.errors[0].type.should.eql('UnauthorizedError'); res.body.errors[0].type.should.eql('UnauthorizedError');
res.body.errors[0].message.should.eql('Invalid token structure'); res.body.errors[0].message.should.eql('Cannot reset password.');
res.body.errors[0].context.should.eql('Invalid password reset link.');
});
});
it('reset password: expired token', function () {
return models.User.getOwnerUser(testUtils.context.internal)
.then(function (ownerUser) {
const dateInThePast = Date.now() - (1000 * 60);
const token = security.tokens.resetToken.generateHash({
expires: dateInThePast,
email: user.email,
dbHash: settingsCache.get('db_hash'),
password: ownerUser.get('password')
});
return request
.put(localUtils.API.getApiQuery('authentication/passwordreset'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.send({
passwordreset: [{
token: token,
newPassword: 'thisissupersafe',
ne2Password: 'thisissupersafe'
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(400);
})
.then((res) => {
should.exist(res.body.errors);
res.body.errors[0].type.should.eql('BadRequestError');
res.body.errors[0].message.should.eql('Cannot reset password.');
res.body.errors[0].context.should.eql('Password reset link expired.');
});
});
it('reset password: unmatched token', function () {
const token = security.tokens.resetToken.generateHash({
expires: Date.now() + (1000 * 60),
email: user.email,
dbHash: settingsCache.get('db_hash'),
password: 'invalid_password'
});
return request
.put(localUtils.API.getApiQuery('authentication/passwordreset'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.send({
passwordreset: [{
token: token,
newPassword: 'thisissupersafe',
ne2Password: 'thisissupersafe'
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(400)
.then((res) => {
should.exist(res.body.errors);
res.body.errors[0].type.should.eql('BadRequestError');
res.body.errors[0].message.should.eql('Cannot reset password.');
res.body.errors[0].context.should.eql('Password reset link has already been used.');
}); });
}); });

View file

@ -556,15 +556,15 @@
dependencies: dependencies:
bluebird "3.7.2" bluebird "3.7.2"
"@tryghost/security@0.1.0": "@tryghost/security@0.2.0":
version "0.1.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/@tryghost/security/-/security-0.1.0.tgz#ce78111dd6febb7705cf62d6838b0d8cdfb7df9f" resolved "https://registry.yarnpkg.com/@tryghost/security/-/security-0.2.0.tgz#3f2ffa10bd933a21b19f659be9636146fb74a790"
integrity sha512-jDiLbsN9zCAZhGIibCb92Ym30BBWzVSZx2DBPL+Ige8b95pefEx6d8humbCjXhMIcJS5dlaMw4Pk70mXLCXCvQ== integrity sha512-plCxkYWD22mCQowH6boGakc2svv6Xqc7W1gJL007baBHHFR4XvWigseTxit1sb6FsI+GpzV16lBLtUUNiYTQTA==
dependencies: dependencies:
"@tryghost/string" "0.1.10" "@tryghost/string" "0.1.11"
bcryptjs "2.4.3" bcryptjs "2.4.3"
bluebird "3.7.2" bluebird "3.7.2"
lodash "4.17.19" lodash "4.17.20"
"@tryghost/session-service@0.1.8": "@tryghost/session-service@0.1.8":
version "0.1.8" version "0.1.8"
@ -582,13 +582,6 @@
ghost-ignition "4.2.2" ghost-ignition "4.2.2"
lodash "4.17.19" lodash "4.17.19"
"@tryghost/string@0.1.10":
version "0.1.10"
resolved "https://registry.yarnpkg.com/@tryghost/string/-/string-0.1.10.tgz#6fb6a16cc628e6c22397c5889a7a85c29171374a"
integrity sha512-TqkuNTvM6zOHIugAc3lnok4U4e4kKLvZz/Oo5Kka1jMHe70SrJZ3X1ljZEn7RloV3PdHmLUE5zN3Zvj2aC1avg==
dependencies:
unidecode "^0.1.8"
"@tryghost/string@0.1.11": "@tryghost/string@0.1.11":
version "0.1.11" version "0.1.11"
resolved "https://registry.yarnpkg.com/@tryghost/string/-/string-0.1.11.tgz#259e101a6fa7d08870e5c3b12091f62eb61c3725" resolved "https://registry.yarnpkg.com/@tryghost/string/-/string-0.1.11.tgz#259e101a6fa7d08870e5c3b12091f62eb61c3725"