diff --git a/README.md b/README.md index 7a9966d..0d57b09 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ There are no default values for `expiresIn`, `notBefore`, `audience`, `subject`, The header can be customized via the `option.header` object. -Generated JWTs will include an `iat` claim by default unless `noTimestamp` is specified. +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 for calculating other things like `exp` given a timespan in `options.expiresIn`. Example @@ -51,6 +51,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 @@ -81,6 +83,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` and `exp` claims, to deal with small clock differences among different servers + ```js // verify a token symmetric - synchronous diff --git a/index.js b/index.js index ba02de0..d20164d 100644 --- a/index.js +++ b/index.js @@ -135,7 +135,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))); } } @@ -144,8 +144,9 @@ 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))); + } } if (options.audience) { @@ -185,7 +186,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) * 1000) { return done(new TokenExpiredError('maxAge exceeded', new Date(payload.iat * 1000 + maxAge))); } } diff --git a/sign.js b/sign.js index f55c92b..23f5254 100644 --- a/sign.js +++ b/sign.js @@ -16,6 +16,13 @@ var sign_options_schema = Joi.object().keys({ noTimestamp: Joi.boolean() }); +var registered_claims_schema = Joi.object().keys({ + iat: Joi.number(), + exp: Joi.number(), + nbf: Joi.number() +}).unknown(); + + var options_to_payload = { 'audience': 'aud', 'issuer': 'iss', @@ -44,6 +51,12 @@ module.exports = function(payload, secretOrPrivateKey, options, callback) { if (typeof payload === 'undefined') { throw new Error('payload is required'); } else if (typeof payload === 'object') { + var payload_validation_result = registered_claims_schema.validate(payload); + + if (payload_validation_result.error) { + throw payload_validation_result.error; + } + payload = xtend(payload); } else if (typeof payload !== 'object') { var invalid_options = options_for_objects.filter(function (opt) { diff --git a/test/iat.tests.js b/test/iat.tests.js new file mode 100644 index 0000000..015ce3a --- /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" must be a number/); + }); + + +}); diff --git a/test/verify.tests.js b/test/verify.tests.js index bd8d9f7..7542e1e 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(); }); }); @@ -50,7 +50,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) {} @@ -70,9 +70,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); @@ -95,10 +106,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'); @@ -108,7 +131,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'); @@ -121,7 +144,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'); @@ -136,7 +159,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');