🐛 Added multiple use grace period to tokens (#12519)

closes https://github.com/TryGhost/Ghost/issues/12347

This change allows a token to be used multiple times for the first 10
seconds after its initial use, this will stop dynamic link checking
software from invaliding magic links.
This commit is contained in:
Fabien 'egg' O'Carroll 2021-01-18 17:03:41 +00:00 committed by GitHub
parent a0303a246e
commit 7fdddf34b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 55 additions and 39 deletions

View File

@ -1,5 +1,6 @@
const ghostBookshelf = require('./base'); const ghostBookshelf = require('./base');
const crypto = require('crypto'); const crypto = require('crypto');
const logging = require('../../shared/logging');
const SingleUseToken = ghostBookshelf.Model.extend({ const SingleUseToken = ghostBookshelf.Model.extend({
tableName: 'tokens', tableName: 'tokens',
@ -16,19 +17,23 @@ const SingleUseToken = ghostBookshelf.Model.extend({
} }
}, { }, {
async findOne(data, unfilteredOptions = {}) { async findOne(data, unfilteredOptions = {}) {
if (!unfilteredOptions.transacting) {
return ghostBookshelf.transaction((transacting) => {
return this.findOne(data, Object.assign({transacting}, unfilteredOptions));
});
}
const model = await ghostBookshelf.Model.findOne.call(this, data, unfilteredOptions); const model = await ghostBookshelf.Model.findOne.call(this, data, unfilteredOptions);
if (model) { if (model) {
setTimeout(async () => {
try {
await this.destroy(Object.assign({ await this.destroy(Object.assign({
destroyBy: { destroyBy: {
id: model.id id: model.id
} }
}, unfilteredOptions)); }, {
...unfilteredOptions,
transacting: null
}));
} catch (err) {
logging.error(err);
}
}, 10000);
} }
return model; return model;

View File

@ -1,30 +0,0 @@
const models = require('../../../core/server/models');
const should = require('should');
describe('Regression: models/single-use-token', function () {
before(function () {
models.init();
});
describe('findOne', function () {
it('Does not allow the same token to be read twice', async function () {
const insertedToken = await models.SingleUseToken.add({
data: 'some_data'
}, {});
const tokenFirstRead = await models.SingleUseToken.findOne({
token: insertedToken.get('token')
});
should.exist(tokenFirstRead);
should.equal(tokenFirstRead.id, insertedToken.id);
const tokenSecondRead = await models.SingleUseToken.findOne({
token: insertedToken.get('token')
});
should.not.exist(tokenSecondRead);
});
});
});

View File

@ -0,0 +1,41 @@
const models = require('../../../core/server/models');
const should = require('should');
const sinon = require('sinon');
let clock;
let sandbox;
describe('Unit: models/single-use-token', function () {
before(function () {
models.init();
sandbox = sinon.createSandbox();
clock = sandbox.useFakeTimers();
});
after(function () {
clock.restore();
sandbox.restore();
});
describe('fn: findOne', function () {
it('Calls destroy after the grace period', async function () {
const data = {};
const options = {};
const fakeModel = {
id: 'fake_id'
};
const findOneSuperStub = sandbox.stub(models.Base.Model, 'findOne').resolves(fakeModel);
const destroyStub = sandbox.stub(models.SingleUseToken, 'destroy').resolves();
await models.SingleUseToken.findOne(data, options);
should.ok(findOneSuperStub.calledWith(data, options), 'super.findOne was called');
clock.tick(10000);
should.ok(destroyStub.called, 'destroy was called after 10 seconds');
});
});
});