diff --git a/ghost/core/core/server/services/members/RequestIntegrityTokenProvider.js b/ghost/core/core/server/services/members/RequestIntegrityTokenProvider.js index 50baacf5b8..7752185830 100644 --- a/ghost/core/core/server/services/members/RequestIntegrityTokenProvider.js +++ b/ghost/core/core/server/services/members/RequestIntegrityTokenProvider.js @@ -7,7 +7,7 @@ class RequestIntegrityTokenProvider { /** * @param {object} options * @param {string} options.themeSecret - * @param {number} options.tokenDuration + * @param {number} options.tokenDuration - in milliseconds */ constructor(options) { this.#themeSecret = options.themeSecret; @@ -37,12 +37,12 @@ class RequestIntegrityTokenProvider { return false; } - const nonce = parts[0]; - const timestamp = parseInt(parts[1], 10); + const timestamp = parseInt(parts[0], 10); + const nonce = parts[1]; const hmacDigest = parts[2]; const hmac = crypto.createHmac('sha256', this.#themeSecret); - hmac.update(`${nonce}:${timestamp.toString()}`); + hmac.update(`${timestamp.toString()}:${nonce}`); const expectedHmac = hmac.digest('hex'); if (expectedHmac !== hmacDigest) { diff --git a/ghost/core/test/unit/server/services/members/RequestIntegrityTokenProvider.test.js b/ghost/core/test/unit/server/services/members/RequestIntegrityTokenProvider.test.js new file mode 100644 index 0000000000..e13c0abd5b --- /dev/null +++ b/ghost/core/test/unit/server/services/members/RequestIntegrityTokenProvider.test.js @@ -0,0 +1,70 @@ +const sinon = require('sinon'); +const should = require('should'); + +const RequestIntegrityTokenProvider = require('../../../../../core/server/services/members/RequestIntegrityTokenProvider'); + +const tokenProvider = new RequestIntegrityTokenProvider({ + themeSecret: 'test', + tokenDuration: 100 +}); + +describe('RequestIntegrityTokenProvider', function () { + beforeEach(function () { + sinon.useFakeTimers(new Date('2021-01-01')); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe('create', function () { + it('should create a HMAC digest from the secret', function () { + const token = tokenProvider.create(); + + token.should.be.a.String(); + token.split(':').should.be.an.Array().with.lengthOf(3); + const [timestamp, nonce, digest] = token.split(':'); + + timestamp.should.equal((new Date('2021-01-01').valueOf() + 100).toString()); + + nonce.should.match(/[0-9a-f]{16}/); + + digest.should.be.a.String().with.lengthOf(64); + }); + }); + + describe('validate', function () { + it('should verify a HMAC digest from the secret', function () { + const token = tokenProvider.create(); + const result = tokenProvider.validate(token); + + result.should.be.true(); + }); + + it('should fail to verify an expired token', function () { + const token = tokenProvider.create(); + sinon.clock.tick(101); + const result = tokenProvider.validate(token); + + result.should.be.false(); + }); + + it('should fail to verify a malformed token', function () { + const token = 'invalid_token'; + const result = tokenProvider.validate(token); + + result.should.be.false(); + }); + + it('should fail to verify a token with an invalid signature', function () { + const token = tokenProvider.create(); + const [timestamp, nonce] = token.split(':'); + const invalidDigest = 'a'.repeat(64); // Create an invalid digest + const invalidToken = `${timestamp}:${nonce}:${invalidDigest}`; + + const result = tokenProvider.validate(invalidToken); + + result.should.be.false(); + }); + }); +});