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(); - }); - }); }); }); });