From a4787e129cc94098a8922737a70667c2ad738e7e Mon Sep 17 00:00:00 2001 From: jacopo_farina Date: Thu, 24 Mar 2016 23:49:35 +0100 Subject: [PATCH 01/12] preliminary version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1912062..f3e82ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonwebtoken", - "version": "5.7.0", + "version": "5.8.0", "description": "JSON Web Token implementation (symmetric and asymmetric)", "main": "index.js", "scripts": { From c7f9a7d3abba4792a7b66302ab9fa951fe72542c Mon Sep 17 00:00:00 2001 From: Jacopo Date: Thu, 24 Mar 2016 23:55:48 +0100 Subject: [PATCH 02/12] specify the use of iat in the payload --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 54db00a..929c681 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ If any `expiresIn`, `notBeforeMinutes`, `audience`, `subject`, `issuer` are not Additional headers can be provided via the `headers` object. -Generated jwts will include an `iat` claim by default unless `noTimestamp` is specified. +Generated jwts will include an `iat` claim by default unless `noTimestamp` is specified. If `iat` is inserted in the payload, it will be used instead of the real payload, which can be useful if you want your JWTs to be verified by servers having a slightly backdated clock. Example @@ -52,6 +52,8 @@ Example // sign with default (HMAC SHA256) var jwt = require('jsonwebtoken'); var token = jwt.sign({ foo: 'bar' }, 'shhhhh'); +//backdate a jwt 30 seconds +var older_token = jwt.sign({ foo: 'bar', iat: Math.floor(Date.now() / 1000) - 30 }, 'shhhhh'); // sign with RSA SHA256 var cert = fs.readFileSync('private.key'); // get private key From f5b60a1bdfcd29c3317c1c9c2d5a20fc126eb7c3 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Thu, 24 Mar 2016 23:59:32 +0100 Subject: [PATCH 03/12] clearer description of iat --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 929c681..e2707f2 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ If any `expiresIn`, `notBeforeMinutes`, `audience`, `subject`, `issuer` are not Additional headers can be provided via the `headers` object. -Generated jwts will include an `iat` claim by default unless `noTimestamp` is specified. If `iat` is inserted in the payload, it will be used instead of the real payload, which can be useful if you want your JWTs to be verified by servers having a slightly backdated clock. +Generated jwts will include an `iat` (issued at) claim by default unless `noTimestamp` is specified. If `iat` is inserted in the payload, it will be used instead of the real timestamp, which can be useful if you want your JWTs to be verified by servers having a slightly backdated clock. Example From e719a2c0a014bdac7fa146d0e9212d141f28b131 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Fri, 25 Mar 2016 00:02:56 +0100 Subject: [PATCH 04/12] check that iat is undefined or a number --- index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.js b/index.js index 9dfbca9..5d61e5d 100644 --- a/index.js +++ b/index.js @@ -77,6 +77,9 @@ JWT.sign = function(payload, secretOrPrivateKey, options, callback) { var timestamp = Math.floor(Date.now() / 1000); if (!options.noTimestamp) { + if (typeof payload.iat !== 'undefined' && typeof payload.iat !== 'number') { + throw new Error('"iat" if present should be a number'); + } payload.iat = payload.iat || timestamp; } From 649f4d90170df58e1a72d7540f8e5f244afb1a52 Mon Sep 17 00:00:00 2001 From: jacopo_farina Date: Fri, 25 Mar 2016 00:12:05 +0100 Subject: [PATCH 05/12] add tests about iat payload validation --- test/iat.tests.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 test/iat.tests.js diff --git a/test/iat.tests.js b/test/iat.tests.js new file mode 100644 index 0000000..a021faf --- /dev/null +++ b/test/iat.tests.js @@ -0,0 +1,20 @@ +var jwt = require('../index'); +var expect = require('chai').expect; + +describe('iat', function() { + + it('should work with a numeric iat not changing the expiration date', function () { + var token = jwt.sign({foo: 123, iat: Math.floor(Date.now() / 1000) - 30}, '123', { expiresIn: 10 }); + var result = jwt.verify(token, '123'); + expect(result.exp).to.be.closeTo(Math.floor(Date.now() / 1000) + 10, 0.2); + }); + + + it('should throw if iat is not a number', function () { + expect(function () { + jwt.sign({foo: 123, iat:'hello'}, '123'); + }).to.throw(/"iat" if present should be a number/); + }); + + +}); From 294e94b6855b75b8ad32d75be1469e6c39e71707 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Fri, 25 Mar 2016 00:17:27 +0100 Subject: [PATCH 06/12] describe clockTolerance meaning --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e2707f2..d83c018 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ encoded public key for RSA and ECDSA. * `ignoreExpiration`: if `true` do not validate the expiration of the token. * `ignoreNotBefore`... * `subject`: if you want to check subject (`sub`), provide a value here +* `clockTolerance`: number of second to tolerate when checking the `nbf` claim, to deal with small clock differences among different servers + ```js // verify a token symmetric - synchronous From 51d844d287370b0dc08d9ec1255e89501989c69a Mon Sep 17 00:00:00 2001 From: Jacopo Date: Fri, 25 Mar 2016 00:20:39 +0100 Subject: [PATCH 07/12] use clockTolerance when verifying --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 5d61e5d..ca0432d 100644 --- a/index.js +++ b/index.js @@ -233,7 +233,7 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) { if (typeof payload.nbf !== 'number') { return done(new JsonWebTokenError('invalid nbf value')); } - if (payload.nbf > Math.floor(Date.now() / 1000)) { + if (payload.nbf > Math.floor(Date.now() / 1000) + (options.clockTolerance || 0)) { return done(new NotBeforeError('jwt not active', new Date(payload.nbf * 1000))); } } @@ -242,7 +242,7 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) { if (typeof payload.exp !== 'number') { return done(new JsonWebTokenError('invalid exp value')); } - if (Math.floor(Date.now() / 1000) >= payload.exp) + if (Math.floor(Date.now() / 1000) >= payload.exp + (options.clockTolerance || 0)) return done(new TokenExpiredError('jwt expired', new Date(payload.exp * 1000))); } From 80a499fb3d5ba7c3ac7ef828046a8691b58e4d70 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Fri, 25 Mar 2016 00:21:59 +0100 Subject: [PATCH 08/12] specify that clockTolerance is both for nbf and exp --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d83c018..e2c9e11 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ encoded public key for RSA and ECDSA. * `ignoreExpiration`: if `true` do not validate the expiration of the token. * `ignoreNotBefore`... * `subject`: if you want to check subject (`sub`), provide a value here -* `clockTolerance`: number of second to tolerate when checking the `nbf` claim, to deal with small clock differences among different servers +* `clockTolerance`: number of second to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers ```js From 26e91cb17fbec02e1eab5ea0d6f52bd69cea88cf Mon Sep 17 00:00:00 2001 From: Jacopo Date: Fri, 25 Mar 2016 00:35:58 +0100 Subject: [PATCH 09/12] clockTolerance for maxAge too --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ca0432d..ddbb882 100644 --- a/index.js +++ b/index.js @@ -283,7 +283,7 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) { if (typeof payload.iat !== 'number') { return done(new JsonWebTokenError('iat required when maxAge is specified')); } - if (Date.now() - (payload.iat * 1000) > maxAge) { + if (Date.now() - (payload.iat * 1000) > maxAge + (options.clockTolerance || 0)) { return done(new TokenExpiredError('maxAge exceeded', new Date(payload.iat * 1000 + maxAge))); } } From 58c865d832b1ed5fea6c8aaa7711b94bfbf4b3d8 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Fri, 25 Mar 2016 00:38:17 +0100 Subject: [PATCH 10/12] maxAge is in milliseconds, not seconds --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ddbb882..8a5361a 100644 --- a/index.js +++ b/index.js @@ -283,7 +283,7 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) { if (typeof payload.iat !== 'number') { return done(new JsonWebTokenError('iat required when maxAge is specified')); } - if (Date.now() - (payload.iat * 1000) > maxAge + (options.clockTolerance || 0)) { + if (Date.now() - (payload.iat * 1000) > maxAge + (options.clockTolerance || 0) * 1000) { return done(new TokenExpiredError('maxAge exceeded', new Date(payload.iat * 1000 + maxAge))); } } From 08a5ff58367a7f944947fd7884e35e3311aa51ae Mon Sep 17 00:00:00 2001 From: jacopo_farina Date: Fri, 25 Mar 2016 00:39:03 +0100 Subject: [PATCH 11/12] add test for clockTolerance --- test/verify.tests.js | 47 +++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/test/verify.tests.js b/test/verify.tests.js index cb71137..ce63a7c 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -15,16 +15,16 @@ describe('verify', function() { var payload = { iat: Math.floor(Date.now() / 1000 ) }; var signed = jws.sign({ - header: header, + header: header, payload: payload, secret: priv, encoding: 'utf8' }); jwt.verify(signed, pub, {typ: 'JWT'}, function(err, p) { - assert.isNull(err); - assert.deepEqual(p, payload); - done(); + assert.isNull(err); + assert.deepEqual(p, payload); + done(); }); }); @@ -32,7 +32,7 @@ describe('verify', function() { // { foo: 'bar', iat: 1437018582, exp: 1437018583 } var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s'; var key = 'key'; - + var clock; afterEach(function () { try { clock.restore(); } catch (e) {} @@ -52,9 +52,20 @@ describe('verify', function() { }); }); - it('should not error on unexpired token', function (done) { - clock = sinon.useFakeTimers(1437018582000); - var options = {algorithms: ['HS256']} + it('should not error on expired token within clockTolerance interval', function (done) { + clock = sinon.useFakeTimers(1437018584000); + var options = {algorithms: ['HS256'], clockTolerance: 100} + + 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) { + clock = sinon.useFakeTimers(1437018582500); + var options = {algorithms: ['HS256'], maxAge: '600ms'}; jwt.verify(token, key, options, function (err, p) { assert.isNull(err); @@ -77,10 +88,22 @@ describe('verify', function() { done(); }); }); + + it('should not error for claims issued before a certain timespan but still inside clockTolerance timespan', function (done) { + clock = sinon.useFakeTimers(1437018582500); + var options = {algorithms: ['HS256'], maxAge: '321ms', clockTolerance: 100}; + + 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) { clock = sinon.useFakeTimers(1437018582500); var options = {algorithms: ['HS256'], maxAge: '600ms'}; - + jwt.verify(token, key, options, function (err, p) { assert.isNull(err); assert.equal(p.foo, 'bar'); @@ -90,7 +113,7 @@ describe('verify', function() { it('can be more restrictive than expiration', function (done) { clock = sinon.useFakeTimers(1437018582900); var options = {algorithms: ['HS256'], maxAge: '800ms'}; - + jwt.verify(token, key, options, function (err, p) { assert.equal(err.name, 'TokenExpiredError'); assert.equal(err.message, 'maxAge exceeded'); @@ -103,7 +126,7 @@ describe('verify', function() { it('cannot be more permissive than expiration', function (done) { clock = sinon.useFakeTimers(1437018583100); var options = {algorithms: ['HS256'], maxAge: '1200ms'}; - + jwt.verify(token, key, options, function (err, p) { // maxAge not exceded, but still expired assert.equal(err.name, 'TokenExpiredError'); @@ -118,7 +141,7 @@ describe('verify', function() { clock = sinon.useFakeTimers(1437018582900); 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'); From ac7e6a6a4a4178ffc18629c95ccd78736bdba463 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Fri, 25 Mar 2016 22:35:53 +0100 Subject: [PATCH 12/12] use the same timestamp in sync and async --- test/async_sign.tests.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/async_sign.tests.js b/test/async_sign.tests.js index 57495a9..5a17a8e 100644 --- a/test/async_sign.tests.js +++ b/test/async_sign.tests.js @@ -6,10 +6,12 @@ describe('signing a token asynchronously', function() { describe('when signing a token', function() { var secret = 'shhhhhh'; - var syncToken = jwt.sign({ foo: 'bar' }, secret, { algorithm: 'HS256' }); + //use the same timestamp, or it may differ in the two tokens + var currentTimestamp = Math.floor(new Date()/1000); + var syncToken = jwt.sign({ foo: 'bar', iat: currentTimestamp }, secret, { algorithm: 'HS256' }); it('should return the same result as singing synchronously', function(done) { - jwt.sign({ foo: 'bar' }, secret, { algorithm: 'HS256' }, function (asyncToken) { + jwt.sign({ foo: 'bar', iat: currentTimestamp }, secret, { algorithm: 'HS256' }, function (asyncToken) { expect(asyncToken).to.be.a('string'); expect(asyncToken.split('.')).to.have.length(3); expect(asyncToken).to.equal(syncToken);