Skip to content

Commit

Permalink
Merge branch 'origin/clock_skew_tolerance' of https://github.com/jaco…
Browse files Browse the repository at this point in the history
…pofar/node-jsonwebtoken into jacopofar-origin/clock_skew_tolerance
  • Loading branch information
jfromaniello committed Apr 29, 2016
2 parents d78659f + ac7e6a6 commit 65ddea9
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 16 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@ There are no default values for `expiresIn`, `notBefore`, `audience`, `subject`,

This comment has been minimized.

Copy link
@jfromaniello

jfromaniello May 19, 2016

Author Member

?

This comment has been minimized.

Copy link
@batmat

batmat Jun 18, 2016

@jfromaniello spammer IMO. That user just posted weird comments in jenkinsci/jenkins@af632dd#commitcomment-17922288 for example. I found that one here through his public activity.

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

```js
// 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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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)));
}
}
Expand Down
13 changes: 13 additions & 0 deletions sign.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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) {
Expand Down
20 changes: 20 additions & 0 deletions test/iat.tests.js
Original file line number Diff line number Diff line change
@@ -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/);
});


});
47 changes: 35 additions & 12 deletions test/verify.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

Expand All @@ -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) {}
Expand All @@ -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);
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand Down

0 comments on commit 65ddea9

Please sign in to comment.