Merge branch 'origin/clock_skew_tolerance' of https://github.com/jacopofar/node-jsonwebtoken into jacopofar-origin/clock_skew_tolerance

This commit is contained in:
José F. Romaniello
2016-04-29 11:30:21 -03:00
5 changed files with 77 additions and 16 deletions
+5 -1
View File
@@ -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
+4 -3
View File
@@ -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)));
}
}
+13
View File
@@ -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) {
+20
View File
@@ -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/);
});
});
+35 -12
View File
@@ -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');