0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Bypass 2FA for accounts which haven't yet logged in

ref ENG-1681
This commit is contained in:
Sam Lord 2024-11-25 14:47:23 +00:00 committed by Sam Lord
parent 781bfdd60f
commit 28dba53f26
6 changed files with 97 additions and 5 deletions

View file

@ -28,9 +28,17 @@ const controller = {
}));
}
return models.User.check({
email: object.username,
password: object.password
let skipVerification = false;
return models.User.getByEmail(object.username).then((user) => {
if (!user.hasLoggedIn()) {
skipVerification = true;
}
return models.User.check({
email: object.username,
password: object.password
});
}).then((user) => {
return Promise.resolve(function sessionMiddleware(req, res, next) {
req.brute.reset(function (err) {
@ -38,6 +46,8 @@ const controller = {
return next(err);
}
req.user = user;
req.skipVerification = skipVerification;
auth.session.createSession(req, res, next);
});
});

View file

@ -161,6 +161,7 @@ module.exports = {
meta_title: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 300}}},
meta_description: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 500}}},
tour: {type: 'text', maxlength: 65535, nullable: true},
// NOTE: Used to determine whether a user has logged in previously
last_seen: {type: 'dateTime', nullable: true},
comment_notifications: {type: 'boolean', nullable: false, defaultTo: true},
free_member_signup_notification: {type: 'boolean', nullable: false, defaultTo: true},

View file

@ -329,6 +329,10 @@ User = ghostBookshelf.Model.extend({
return this.save();
},
hasLoggedIn: function hasLoggedIn() {
return Boolean(this.get('last_seen'));
},
enforcedFilters: function enforcedFilters(options) {
if (options.context && options.context.internal) {
return null;

View file

@ -3,7 +3,11 @@ const errors = require('@tryghost/errors');
function SessionMiddleware({sessionService}) {
async function createSession(req, res, next) {
try {
await sessionService.createSessionForUser(req, res, req.user);
if (req.skipVerification) {
await sessionService.createVerifiedSessionForUser(req, res, req.user);
} else {
await sessionService.createSessionForUser(req, res, req.user);
}
const isVerified = await sessionService.isVerifiedSession(req, res);
if (isVerified) {

View file

@ -57,6 +57,8 @@ describe('Session controller', function () {
const fakeUser = models.User.forge({});
sinon.stub(models.User, 'check')
.resolves(fakeUser);
sinon.stub(models.User, 'getByEmail')
.resolves(fakeUser);
const createSessionStub = sinon.stub(sessionServiceMiddleware, 'createSession');
@ -88,6 +90,8 @@ describe('Session controller', function () {
const fakeUser = models.User.forge({});
sinon.stub(models.User, 'check')
.resolves(fakeUser);
sinon.stub(models.User, 'getByEmail')
.resolves(fakeUser);
sinon.stub(sessionServiceMiddleware, 'createSession');
@ -102,6 +106,74 @@ describe('Session controller', function () {
should.equal(fakeNext.args[0][0], resetError);
});
});
it('it creates a verified session when the user has not logged in before', function () {
const fakeReq = {
brute: {
reset: sinon.stub().callsArg(0)
}
};
const fakeRes = {};
const fakeNext = () => {};
const fakeUser = models.User.forge({});
sinon.stub(models.User, 'check')
.resolves(fakeUser);
sinon.stub(models.User, 'getByEmail')
.resolves(fakeUser);
const createSessionStub = sinon.stub(sessionServiceMiddleware, 'createSession');
return sessionController.add({data: {
username: 'freddy@vodafone.com',
password: 'qu33nRul35'
}}).then((fn) => {
fn(fakeReq, fakeRes, fakeNext);
}).then(function () {
should.equal(fakeReq.brute.reset.callCount, 1);
const createSessionStubCall = createSessionStub.getCall(0);
should.equal(fakeReq.user, fakeUser);
should.equal(createSessionStubCall.args[0], fakeReq);
should.equal(createSessionStubCall.args[1], fakeRes);
should.equal(createSessionStubCall.args[2], fakeNext);
should.equal(fakeReq.skipVerification, true);
});
});
it('it creates a non-verified session when the user has logged in before', function () {
const fakeReq = {
brute: {
reset: sinon.stub().callsArg(0)
}
};
const fakeRes = {};
const fakeNext = () => {};
const fakeUser = models.User.forge({last_seen: new Date()});
sinon.stub(models.User, 'check')
.resolves(fakeUser);
sinon.stub(models.User, 'getByEmail')
.resolves(fakeUser);
const createSessionStub = sinon.stub(sessionServiceMiddleware, 'createSession');
return sessionController.add({data: {
username: 'freddy@vodafone.com',
password: 'qu33nRul35'
}}).then((fn) => {
fn(fakeReq, fakeRes, fakeNext);
}).then(function () {
should.equal(fakeReq.brute.reset.callCount, 1);
const createSessionStubCall = createSessionStub.getCall(0);
should.equal(fakeReq.user, fakeUser);
should.equal(createSessionStubCall.args[0], fakeReq);
should.equal(createSessionStubCall.args[1], fakeRes);
should.equal(createSessionStubCall.args[2], fakeNext);
should.equal(fakeReq.skipVerification, false);
});
});
});
describe('#delete', function () {

View file

@ -134,7 +134,8 @@ DataGenerator.Content = {
email: 'jbloggs@example.com',
password: 'Sl1m3rson99',
profile_image: 'https://example.com/super_photo.jpg',
paid_subscription_canceled_notification: true
paid_subscription_canceled_notification: true,
last_seen: moment().subtract(1, 'hour').toDate()
},
{
// admin