diff --git a/core/server/api/canary/authentication.js b/core/server/api/canary/authentication.js
index ebd5918bc4..a2d6bcd58a 100644
--- a/core/server/api/canary/authentication.js
+++ b/core/server/api/canary/authentication.js
@@ -108,7 +108,11 @@ module.exports = {
return auth.passwordreset.generateToken(frame.data.passwordreset[0].email, api.settings);
})
.then((token) => {
- return auth.passwordreset.sendResetNotification(token, api.mail);
+ if (frame.data.required) {
+ return auth.passwordreset.sendRequiredResetNotification(token, api.mail);
+ } else {
+ return auth.passwordreset.sendResetNotification(token, api.mail);
+ }
});
}
},
diff --git a/core/server/api/canary/session.js b/core/server/api/canary/session.js
index f57ee94167..2db7f1ef5b 100644
--- a/core/server/api/canary/session.js
+++ b/core/server/api/canary/session.js
@@ -2,6 +2,7 @@ const Promise = require('bluebird');
const common = require('../../lib/common');
const models = require('../../models');
const auth = require('../../services/auth');
+const api = require('./index');
const session = {
read(frame) {
@@ -35,11 +36,24 @@ const session = {
auth.session.createSession(req, res, next);
});
});
- }).catch((err) => {
- throw new common.errors.UnauthorizedError({
- message: common.i18n.t('errors.middleware.auth.accessDenied'),
- err
- });
+ }).catch(async (err) => {
+ if (!common.errors.utils.isIgnitionError(err)) {
+ throw new common.errors.UnauthorizedError({
+ message: common.i18n.t('errors.middleware.auth.accessDenied'),
+ err
+ });
+ }
+
+ if (err.errorType === 'PasswordResetRequiredError') {
+ await api.authentication.generateResetToken({
+ passwordreset: [{
+ email: object.username
+ }],
+ required: true
+ }, frame.options.context);
+ }
+
+ throw err;
});
},
delete() {
diff --git a/core/server/api/v2/authentication.js b/core/server/api/v2/authentication.js
index ebd5918bc4..a2d6bcd58a 100644
--- a/core/server/api/v2/authentication.js
+++ b/core/server/api/v2/authentication.js
@@ -108,7 +108,11 @@ module.exports = {
return auth.passwordreset.generateToken(frame.data.passwordreset[0].email, api.settings);
})
.then((token) => {
- return auth.passwordreset.sendResetNotification(token, api.mail);
+ if (frame.data.required) {
+ return auth.passwordreset.sendRequiredResetNotification(token, api.mail);
+ } else {
+ return auth.passwordreset.sendResetNotification(token, api.mail);
+ }
});
}
},
diff --git a/core/server/api/v2/session.js b/core/server/api/v2/session.js
index f57ee94167..2db7f1ef5b 100644
--- a/core/server/api/v2/session.js
+++ b/core/server/api/v2/session.js
@@ -2,6 +2,7 @@ const Promise = require('bluebird');
const common = require('../../lib/common');
const models = require('../../models');
const auth = require('../../services/auth');
+const api = require('./index');
const session = {
read(frame) {
@@ -35,11 +36,24 @@ const session = {
auth.session.createSession(req, res, next);
});
});
- }).catch((err) => {
- throw new common.errors.UnauthorizedError({
- message: common.i18n.t('errors.middleware.auth.accessDenied'),
- err
- });
+ }).catch(async (err) => {
+ if (!common.errors.utils.isIgnitionError(err)) {
+ throw new common.errors.UnauthorizedError({
+ message: common.i18n.t('errors.middleware.auth.accessDenied'),
+ err
+ });
+ }
+
+ if (err.errorType === 'PasswordResetRequiredError') {
+ await api.authentication.generateResetToken({
+ passwordreset: [{
+ email: object.username
+ }],
+ required: true
+ }, frame.options.context);
+ }
+
+ throw err;
});
},
delete() {
diff --git a/core/server/lib/common/errors.js b/core/server/lib/common/errors.js
index 08086fc71c..2c92531f1a 100644
--- a/core/server/lib/common/errors.js
+++ b/core/server/lib/common/errors.js
@@ -67,6 +67,13 @@ const ghostErrors = {
errorType: 'HelperWarning',
hideStack: true
}, options));
+ },
+ PasswordResetRequiredError: function PasswordResetRequiredError(options) {
+ GhostError.call(this, merge({
+ errorType: 'PasswordResetRequiredError',
+ statusCode: 401,
+ message: 'For security, you need to create a new password. An email has been sent to you with instructions!'
+ }, options));
}
};
diff --git a/core/server/models/user.js b/core/server/models/user.js
index 53400c2f32..c5695f3548 100644
--- a/core/server/models/user.js
+++ b/core/server/models/user.js
@@ -805,9 +805,7 @@ User = ghostBookshelf.Model.extend({
}
if (user.isLocked()) {
- throw new common.errors.NoPermissionError({
- message: common.i18n.t('errors.models.user.accountLocked')
- });
+ throw new common.errors.PasswordResetRequiredError();
}
if (user.isInactive()) {
diff --git a/core/server/services/auth/passwordreset.js b/core/server/services/auth/passwordreset.js
index 781d71786b..7099cf27b7 100644
--- a/core/server/services/auth/passwordreset.js
+++ b/core/server/services/auth/passwordreset.js
@@ -145,10 +145,37 @@ async function sendResetNotification(data, mailAPI) {
return mailAPI.send(payload, {context: {internal: true}});
}
+async function sendRequiredResetNotification(data, mailAPI) {
+ const adminUrl = urlUtils.urlFor('admin', true);
+ const resetUrl = urlUtils.urlJoin(adminUrl, 'reset', security.url.encodeBase64(data.resetToken), '/');
+
+ const content = await mail.utils.generateContent({
+ data: {
+ resetUrl: resetUrl
+ },
+ template: 'reset-password-required'
+ });
+
+ const payload = {
+ mail: [{
+ message: {
+ to: data.email,
+ subject: i18n.t('common.api.authentication.mail.resetPasswordRequired'),
+ html: content.html,
+ text: content.text
+ },
+ options: {}
+ }]
+ };
+
+ return mailAPI.send(payload, {context: {internal: true}});
+}
+
module.exports = {
- generateToken: generateToken,
- extractTokenParts: extractTokenParts,
- protectBruteForce: protectBruteForce,
- doReset: doReset,
- sendResetNotification: sendResetNotification
+ generateToken,
+ extractTokenParts,
+ protectBruteForce,
+ doReset,
+ sendResetNotification,
+ sendRequiredResetNotification
};
diff --git a/core/server/services/mail/templates/reset-password-required.html b/core/server/services/mail/templates/reset-password-required.html
new file mode 100644
index 0000000000..c0795abd00
--- /dev/null
+++ b/core/server/services/mail/templates/reset-password-required.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello!
+ For security, it's necessary to reset your password on {{ siteUrl }}.
+ Please follow the link below to complete the process:
Click here to reset your password
+ Alternatively please visit your site's admin area and follow the forgot password process.
+ Ghost
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
diff --git a/core/server/services/permissions/providers.js b/core/server/services/permissions/providers.js
index ee0b0b7d89..bd9c6c5f7c 100644
--- a/core/server/services/permissions/providers.js
+++ b/core/server/services/permissions/providers.js
@@ -6,7 +6,7 @@ const {i18n} = require('../../lib/common');
module.exports = {
user: function (id) {
- return models.User.findOne({id: id, status: 'active'}, {withRelated: ['permissions', 'roles', 'roles.permissions']})
+ return models.User.findOne({id: id}, {withRelated: ['permissions', 'roles', 'roles.permissions']})
.then(function (foundUser) {
// CASE: {context: {user: id}} where the id is not in our database
if (!foundUser) {
@@ -15,6 +15,10 @@ module.exports = {
}));
}
+ if (foundUser.get('status') !== 'active') {
+ return Promise.reject(new errors.UnauthorizedError());
+ }
+
const seenPerms = {};
const rolePerms = _.map(foundUser.related('roles').models, function (role) {
diff --git a/core/server/translations/en.json b/core/server/translations/en.json
index ceaeb735b3..93bbbf7499 100644
--- a/core/server/translations/en.json
+++ b/core/server/translations/en.json
@@ -12,6 +12,7 @@
"sampleBlogDescription": "Thoughts, stories and ideas.",
"mail": {
"resetPassword": "Reset Password",
+ "resetPasswordRequired": "Reset Password",
"checkEmailForInstructions": "Check your email for further instructions.",
"passwordChanged": "Password changed successfully.",
"invitationAccepted": "Invitation accepted.",
@@ -227,7 +228,6 @@
"help": "Visit and save your profile after logging in to check for problems."
},
"incorrectPassword": "Your password is incorrect.",
- "accountLocked": "Your account is locked. Please reset your password to log in again by clicking the \"Forgotten password?\" link!",
"accountSuspended": "Your account was suspended.",
"newPasswordsDoNotMatch": "Your new passwords do not match",
"passwordRequiredForOperation": "Password is required for this operation",
diff --git a/test/unit/models/user_spec.js b/test/unit/models/user_spec.js
index 361912480c..a9cd07395a 100644
--- a/test/unit/models/user_spec.js
+++ b/test/unit/models/user_spec.js
@@ -135,6 +135,20 @@ describe('Unit: models/user', function () {
(err instanceof common.errors.ValidationError).should.eql(true);
});
});
+
+ it('status is locked', function () {
+ const user = models.User.forge(testUtils.DataGenerator.forKnex.createUser({
+ status: 'locked',
+ email: 'test@ghost.de'
+ }));
+
+ sinon.stub(models.User, 'getByEmail').resolves(user);
+
+ return models.User.check({email: user.get('email'), password: 'test'})
+ .catch(function (err) {
+ (err instanceof common.errors.PasswordResetRequiredError).should.eql(true);
+ });
+ });
});
describe('permissible', function () {
diff --git a/test/unit/services/permissions/providers_spec.js b/test/unit/services/permissions/providers_spec.js
index 1d2dd4c79a..6a3717df99 100644
--- a/test/unit/services/permissions/providers_spec.js
+++ b/test/unit/services/permissions/providers_spec.js
@@ -36,6 +36,7 @@ describe('Permission Providers', function () {
const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () {
// Create a fake model
const fakeUser = models.User.forge(testUtils.DataGenerator.Content.users[0]);
+ fakeUser.set('status', 'active');
// Roles & Permissions need to be collections
const fakeAdminRole = models.Roles.forge(testUtils.DataGenerator.Content.roles[0]);
@@ -83,6 +84,7 @@ describe('Permission Providers', function () {
const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () {
// Create a fake model
const fakeUser = models.User.forge(testUtils.DataGenerator.Content.users[0]);
+ fakeUser.set('status', 'active');
// Roles & Permissions need to be collections
const fakeAdminRole = models.Roles.forge(testUtils.DataGenerator.Content.roles[0]);
@@ -132,6 +134,7 @@ describe('Permission Providers', function () {
const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () {
// Create a fake model
const fakeUser = models.User.forge(testUtils.DataGenerator.Content.users[0]);
+ fakeUser.set('status', 'active');
// Roles & Permissions need to be collections
const fakeAdminRole = models.Roles.forge(testUtils.DataGenerator.Content.roles[0]);
@@ -176,6 +179,28 @@ describe('Permission Providers', function () {
})
.catch(done);
});
+
+ it('throws when user with non-active status is loaded', function (done) {
+ // This test requires quite a lot of unique setup work
+ const findUserSpy = sinon.stub(models.User, 'findOne').callsFake(function () {
+ // Create a fake model
+ const fakeUser = models.User.forge(testUtils.DataGenerator.Content.users[0]);
+ fakeUser.set('status', 'locked');
+
+ return Promise.resolve(fakeUser);
+ });
+
+ // Get permissions for the user
+ providers.user(1)
+ .then(function (res) {
+ done(new Error('Locked user should should throw an error'));
+ })
+ .catch((err) => {
+ err.errorType.should.equal('UnauthorizedError');
+ findUserSpy.callCount.should.eql(1);
+ done();
+ });
+ });
});
describe('API Key', function () {