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:
parent
6dc8d91ace
commit
32b37d7ba8
5 changed files with 111 additions and 22 deletions
|
@ -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({
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Add table
Reference in a new issue