From 877bd57ab2aca9b7d230805b21f921baed3da169 Mon Sep 17 00:00:00 2001 From: Tim Oram Date: Sun, 26 Aug 2018 08:22:15 -0230 Subject: [PATCH] Refactor tests related to iat and maxAge (#507) This change extracts all tests related to the iat claim and the maxAge verify option into two test files. Several additional tests are added that were missing from the existing tests. --- test/claim-iat.test.js | 273 +++++++++++++++++++++++++++++++++++++ test/option-maxAge.test.js | 70 ++++++++++ test/schema.tests.js | 7 - test/verify.tests.js | 179 ------------------------ 4 files changed, 343 insertions(+), 186 deletions(-) create mode 100644 test/claim-iat.test.js create mode 100644 test/option-maxAge.test.js diff --git a/test/claim-iat.test.js b/test/claim-iat.test.js new file mode 100644 index 0000000..01358e0 --- /dev/null +++ b/test/claim-iat.test.js @@ -0,0 +1,273 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const sinon = require('sinon'); +const util = require('util'); +const testUtils = require('./test-utils'); + +const base64UrlEncode = testUtils.base64UrlEncode; +const noneAlgorithmHeader = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0'; + +function signWithIssueAtSync(issueAt, options) { + const payload = {}; + if (issueAt !== undefined) { + payload.iat = issueAt; + } + const opts = Object.assign({algorithm: 'none'}, options); + return jwt.sign(payload, undefined, opts); +} + +function signWithIssueAtAsync(issueAt, options, cb) { + const payload = {}; + if (issueAt !== undefined) { + payload.iat = issueAt; + } + const opts = Object.assign({algorithm: 'none'}, options); + // async calls require a truthy secret + // see: https://github.com/brianloveswords/node-jws/issues/62 + return jwt.sign(payload, 'secret', opts, cb); +} + +function verifyWithIssueAtSync(token, maxAge, options) { + const opts = Object.assign({maxAge}, options); + return jwt.verify(token, undefined, opts) +} + +function verifyWithIssueAtAsync(token, maxAge, options, cb) { + const opts = Object.assign({maxAge}, options); + return jwt.verify(token, undefined, opts, cb) +} + +describe('issue at', function() { + describe('`jwt.sign` "iat" claim validation', function () { + [ + true, + false, + null, + '', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((iat) => { + it(`should error with iat of ${util.inspect(iat)}`, function (done) { + expect(() => signWithIssueAtSync(iat, {})).to.throw('"iat" should be a number of seconds'); + signWithIssueAtAsync(iat, {}, (err) => { + expect(err.message).to.equal('"iat" should be a number of seconds'); + done(); + }); + }); + }); + + // undefined needs special treatment because {} is not the same as {iat: undefined} + it('should error with iat of undefined', function (done) { + expect(() => jwt.sign({iat: undefined}, undefined, {algorithm: 'none'})).to.throw( + '"iat" should be a number of seconds' + ); + jwt.sign({iat: undefined}, undefined, {algorithm: 'none'}, (err) => { + expect(err.message).to.equal('"iat" should be a number of seconds'); + done(); + }); + }); + }); + + describe('"iat" in payload with "maxAge" option validation', function () { + [ + true, + false, + null, + undefined, + -Infinity, + Infinity, + NaN, + '', + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((iat) => { + it(`should error with iat of ${util.inspect(iat)}`, function (done) { + const encodedPayload = base64UrlEncode(JSON.stringify({iat})); + const token = `${noneAlgorithmHeader}.${encodedPayload}.`; + expect(() => verifyWithIssueAtSync(token, '1 min', {})).to.throw( + jwt.JsonWebTokenError, 'iat required when maxAge is specified' + ); + + verifyWithIssueAtAsync(token, '1 min', {}, (err) => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err.message).to.equal('iat required when maxAge is specified'); + done(); + }); + }); + }) + }); + + describe('when signing a token', function () { + let fakeClock; + beforeEach(function () { + fakeClock = sinon.useFakeTimers({now: 60000}); + }); + + afterEach(function () { + fakeClock.uninstall(); + }); + + [ + { + description: 'should default to current time for "iat"', + iat: undefined, + expectedIssueAt: 60, + options: {} + }, + { + description: 'should sign with provided time for "iat"', + iat: 100, + expectedIssueAt: 100, + options: {} + }, + // TODO an iat of -Infinity should fail validation + { + description: 'should set null "iat" when given -Infinity', + iat: -Infinity, + expectedIssueAt: null, + options: {} + }, + // TODO an iat of Infinity should fail validation + { + description: 'should set null "iat" when given Infinity', + iat: Infinity, + expectedIssueAt: null, + options: {} + }, + // TODO an iat of NaN should fail validation + { + description: 'should set to current time for "iat" when given value NaN', + iat: NaN, + expectedIssueAt: 60, + options: {} + }, + { + description: 'should remove default "iat" with "noTimestamp" option', + iat: undefined, + expectedIssueAt: undefined, + options: {noTimestamp: true} + }, + { + description: 'should remove provided "iat" with "noTimestamp" option', + iat: 10, + expectedIssueAt: undefined, + options: {noTimestamp: true} + }, + ].forEach((testCase) => { + it(testCase.description, function (done) { + const token = signWithIssueAtSync(testCase.iat, testCase.options); + expect(jwt.decode(token).iat).to.equal(testCase.expectedIssueAt); + signWithIssueAtAsync(testCase.iat, testCase.options, (err, token) => { + // node-jsw catches the error from expect, so we have to wrap it in try/catch and use done(error) + try { + expect(err).to.be.null; + expect(jwt.decode(token).iat).to.equal(testCase.expectedIssueAt); + done(); + } + catch (e) { + done(e); + } + }); + }); + }); + }); + + describe('when verifying a token', function() { + let token; + let fakeClock; + + beforeEach(function() { + fakeClock = sinon.useFakeTimers({now: 60000}); + }); + + afterEach(function () { + fakeClock.uninstall(); + }); + + [ + { + description: 'should verify using "iat" before the "maxAge"', + clockAdvance: 10000, + maxAge: 11, + options: {}, + }, + { + description: 'should verify using "iat" before the "maxAge" with a provided "clockTimestamp', + clockAdvance: 60000, + maxAge: 11, + options: {clockTimestamp: 70}, + }, + { + description: 'should verify using "iat" after the "maxAge" but within "clockTolerance"', + clockAdvance: 10000, + maxAge: 9, + options: {clockTimestamp: 2}, + }, + ].forEach((testCase) => { + it(testCase.description, function (done) { + const token = signWithIssueAtSync(undefined, {}); + fakeClock.tick(testCase.clockAdvance); + expect(verifyWithIssueAtSync(token, testCase.maxAge, testCase.options)).to.not.throw; + verifyWithIssueAtAsync(token, testCase.maxAge, testCase.options, done) + }); + }); + + [ + { + description: 'should throw using "iat" equal to the "maxAge"', + clockAdvance: 10000, + maxAge: 10, + options: {}, + expectedError: 'maxAge exceeded', + expectedExpiresAt: 70000, + }, + { + description: 'should throw using "iat" after the "maxAge"', + clockAdvance: 10000, + maxAge: 9, + options: {}, + expectedError: 'maxAge exceeded', + expectedExpiresAt: 69000, + }, + { + description: 'should throw using "iat" after the "maxAge" with a provided "clockTimestamp', + clockAdvance: 60000, + maxAge: 10, + options: {clockTimestamp: 70}, + expectedError: 'maxAge exceeded', + expectedExpiresAt: 70000, + }, + { + description: 'should throw using "iat" after the "maxAge" and "clockTolerance', + clockAdvance: 10000, + maxAge: 8, + options: {clockTolerance: 2}, + expectedError: 'maxAge exceeded', + expectedExpiresAt: 68000, + }, + ].forEach((testCase) => { + it(testCase.description, function(done) { + const expectedExpiresAtDate = new Date(testCase.expectedExpiresAt); + token = signWithIssueAtSync(undefined, {}); + fakeClock.tick(testCase.clockAdvance); + expect(() => verifyWithIssueAtSync(token, testCase.maxAge, {})) + .to.throw(jwt.TokenExpiredError, testCase.expectedError) + .to.have.property('expiredAt').that.deep.equals(expectedExpiresAtDate); + verifyWithIssueAtAsync(token, testCase.maxAge, {}, (err) => { + expect(err).to.be.instanceOf(jwt.TokenExpiredError); + expect(err.message).to.equal(testCase.expectedError); + expect(err.expiredAt).to.deep.equal(expectedExpiresAtDate); + done(); + }); + }); + }); + }); +}); diff --git a/test/option-maxAge.test.js b/test/option-maxAge.test.js new file mode 100644 index 0000000..c76676f --- /dev/null +++ b/test/option-maxAge.test.js @@ -0,0 +1,70 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const sinon = require('sinon'); +const util = require('util'); + +describe('maxAge option', function() { + let token; + + let fakeClock; + beforeEach(function() { + fakeClock = sinon.useFakeTimers({now: 60000}); + token = jwt.sign({iat: 70}, undefined, {algorithm: 'none'}); + }); + + afterEach(function() { + fakeClock.uninstall(); + }); + + [ + { + description: 'should work with a positive string value', + maxAge: '3s', + }, + { + description: 'should work with a negative string value', + maxAge: '-3s', + }, + { + description: 'should work with a positive numeric value', + maxAge: 3, + }, + { + description: 'should work with a negative numeric value', + maxAge: -3, + }, + ].forEach((testCase) => { + it(testCase.description, function (done) { + expect(jwt.verify(token, undefined, {maxAge: '3s'})).to.not.throw; + jwt.verify(token, undefined, {maxAge: testCase.maxAge}, (err) => { + expect(err).to.be.null; + done(); + }) + }); + }); + + [ + true, + 'invalid', + [], + ['foo'], + {}, + {foo: 'bar'}, + ].forEach((maxAge) => { + it(`should error with value ${util.inspect(maxAge)}`, function (done) { + expect(() => jwt.verify(token, undefined, {maxAge})).to.throw( + jwt.JsonWebTokenError, + '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60' + ); + jwt.verify(token, undefined, {maxAge}, (err) => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err.message).to.equal( + '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60' + ); + done(); + }) + }); + }); +}); diff --git a/test/schema.tests.js b/test/schema.tests.js index 77592d6..924bf70 100644 --- a/test/schema.tests.js +++ b/test/schema.tests.js @@ -73,13 +73,6 @@ describe('schema', function() { jwt.sign(payload, 'foo123'); } - it('should validate iat', function () { - expect(function () { - sign({ iat: '1 monkey' }); - }).to.throw(/"iat" should be a number of seconds/); - sign({ iat: 10.1 }); - }); - it('should validate exp', function () { expect(function () { sign({ exp: '1 monkey' }); diff --git a/test/verify.tests.js b/test/verify.tests.js index ac3f1d3..29bbe10 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -188,122 +188,6 @@ describe('verify', function() { }); }); - it('should not error if within maxAge timespan', function (done) { - clock = sinon.useFakeTimers(1437018587500); // iat + 5.5s, exp - 4.5s - var options = {algorithms: ['HS256'], maxAge: '6s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - - describe('option: maxAge', function () { - - [String('3s'), '3s', 3].forEach(function(maxAge) { - it(`should error for claims issued before a certain timespan (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018587000); // iat + 5s, exp - 5s - var options = {algorithms: ['HS256'], maxAge: maxAge}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'TokenExpiredError'); - assert.equal(err.message, 'maxAge exceeded'); - assert.equal(err.expiredAt.constructor.name, 'Date'); - assert.equal(Number(err.expiredAt), 1437018585000); - assert.isUndefined(p); - done(); - }); - }); - }); - - [String('5s'), '5s', 5].forEach(function (maxAge) { - it(`should not error for claims issued before a certain timespan but still inside clockTolerance timespan (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018587500); // iat + 5.5s, exp - 4.5s - var options = {algorithms: ['HS256'], maxAge: maxAge, clockTolerance: 1 }; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - }); - - [String('6s'), '6s', 6].forEach(function (maxAge) { - it(`should not error if within maxAge timespan (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018587500);// iat + 5.5s, exp - 4.5s - var options = {algorithms: ['HS256'], maxAge: maxAge}; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - }); - - [String('8s'), '8s', 8].forEach(function (maxAge) { - it(`can be more restrictive than expiration (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018591900); // iat + 9.9s, exp - 0.1s - var options = {algorithms: ['HS256'], maxAge: maxAge }; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'TokenExpiredError'); - assert.equal(err.message, 'maxAge exceeded'); - assert.equal(err.expiredAt.constructor.name, 'Date'); - assert.equal(Number(err.expiredAt), 1437018590000); - assert.isUndefined(p); - done(); - }); - }); - }); - - [String('12s'), '12s', 12].forEach(function (maxAge) { - it(`cannot be more permissive than expiration (${typeof maxAge} type)`, function (done) { - clock = sinon.useFakeTimers(1437018593000); // iat + 11s, exp + 1s - var options = {algorithms: ['HS256'], maxAge: '12s'}; - - jwt.verify(token, key, options, function (err, p) { - // maxAge not exceded, but still expired - assert.equal(err.name, 'TokenExpiredError'); - assert.equal(err.message, 'jwt expired'); - assert.equal(err.expiredAt.constructor.name, 'Date'); - assert.equal(Number(err.expiredAt), 1437018592000); - assert.isUndefined(p); - done(); - }); - }); - }); - - [new String('1s'), 'no-timespan-string'].forEach(function (maxAge){ - it(`should error if maxAge is specified with a wrong string format/type (value: ${maxAge}, type: ${typeof maxAge})`, function (done) { - clock = sinon.useFakeTimers(1437018587000); // iat + 5s, exp - 5s - var options = { algorithms: ['HS256'], maxAge: maxAge }; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'JsonWebTokenError'); - assert.equal(err.message, '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'); - assert.isUndefined(p); - done(); - }); - }); - }); - - it('should error if maxAge is specified but there is no iat claim', function (done) { - var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.0MBPd4Bru9-fK_HY3xmuDAc6N_embknmNuhdb9bKL_U'; - var options = {algorithms: ['HS256'], maxAge: '1s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'JsonWebTokenError'); - assert.equal(err.message, 'iat required when maxAge is specified'); - assert.isUndefined(p); - done(); - }); - }); - - }); - describe('option: clockTimestamp', function () { var clockTimestamp = 1000000000; it('should verify unexpired token relative to user-provided clockTimestamp', function (done) { @@ -338,57 +222,6 @@ describe('verify', function() { describe('option: maxAge and clockTimestamp', function () { // { foo: 'bar', iat: 1437018582, exp: 1437018800 } exp = iat + 218s var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA'; - it('should error for claims issued before a certain timespan', function (done) { - var clockTimestamp = 1437018682; - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1m'}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'TokenExpiredError'); - assert.equal(err.message, 'maxAge exceeded'); - assert.equal(err.expiredAt.constructor.name, 'Date'); - assert.equal(Number(err.expiredAt), 1437018642000); - assert.isUndefined(p); - done(); - }); - }); - it('should not error for claims issued before a certain timespan but still inside clockTolerance timespan', function (done) { - var clockTimestamp = 1437018592; // iat + 10s - var options = { - algorithms: ['HS256'], - clockTimestamp: clockTimestamp, - maxAge: '3s', - clockTolerance: 10 - }; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - it('should not error if within maxAge timespan', function (done) { - var clockTimestamp = 1437018587; // iat + 5s - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '6s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.isNull(err); - assert.equal(p.foo, 'bar'); - done(); - }); - }); - it('can be more restrictive than expiration', function (done) { - var clockTimestamp = 1437018588; // iat + 6s - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '5s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'TokenExpiredError'); - assert.equal(err.message, 'maxAge exceeded'); - assert.equal(err.expiredAt.constructor.name, 'Date'); - assert.equal(Number(err.expiredAt), 1437018587000); - assert.isUndefined(p); - done(); - }); - }); it('cannot be more permissive than expiration', function (done) { var clockTimestamp = 1437018900; // iat + 318s (exp: iat + 218s) var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'}; @@ -403,18 +236,6 @@ describe('verify', function() { done(); }); }); - it('should error if maxAge is specified but there is no iat claim', function (done) { - var clockTimestamp = 1437018582; - var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.0MBPd4Bru9-fK_HY3xmuDAc6N_embknmNuhdb9bKL_U'; - var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1s'}; - - jwt.verify(token, key, options, function (err, p) { - assert.equal(err.name, 'JsonWebTokenError'); - assert.equal(err.message, 'iat required when maxAge is specified'); - assert.isUndefined(p); - done(); - }); - }); }); }); });