Skip to content

Add clockTimestamp option to .verify() you can set the current time in seconds with it #274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ encoded public key for RSA and ECDSA.
* `ignoreNotBefore`...
* `subject`: if you want to check subject (`sub`), provide a value here
* `clockTolerance`: number of seconds to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers
* `maxAge`: the maximum allowed age in milliseconds for tokens to still be valid. We advise against using milliseconds precision, though, since JWTs can only contain seconds. The maximum precision might be reduced to seconds in the future
* `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons (also against `maxAge`, so our advise is to avoid using `clockTimestamp` and a `maxAge` in milliseconds together)


```js
Expand Down
141 changes: 140 additions & 1 deletion test/verify.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,145 @@ describe('verify', function() {
});
});
});
});

describe('option: clockTimestamp', function () {
var clockTimestamp = 1000000000;
it('should verify unexpired token relative to user-provided clockTimestamp', function (done) {
var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key);
jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err, p) {
assert.isNull(err);
done();
});
});
it('should error on expired token relative to user-provided clockTimestamp', function (done) {
var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key);
jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err, p) {
assert.equal(err.name, 'TokenExpiredError');
assert.equal(err.message, 'jwt expired');
assert.equal(err.expiredAt.constructor.name, 'Date');
assert.equal(Number(err.expiredAt), (clockTimestamp + 1) * 1000);
assert.isUndefined(p);
done();
});
});
it('should verify clockTimestamp is a number', function (done) {
var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key);
jwt.verify(token, key, {clockTimestamp: 'notANumber'}, function (err, p) {
assert.equal(err.name, 'JsonWebTokenError');
assert.equal(err.message,'clockTimestamp must be a number');
assert.isUndefined(p);
done();
});
});
it('should verify valid token with nbf', function (done) {
var token = jwt.sign({
foo: 'bar',
iat: clockTimestamp,
nbf: clockTimestamp + 1,
exp: clockTimestamp + 2
}, key);
jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err, p) {
assert.isNull(err);
done();
});
});
it('should error on token used before nbf', function (done) {
var token = jwt.sign({
foo: 'bar',
iat: clockTimestamp,
nbf: clockTimestamp + 1,
exp: clockTimestamp + 2
}, key);
jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err, p) {
assert.equal(err.name, 'NotBeforeError');
assert.equal(err.date.constructor.name, 'Date');
assert.equal(Number(err.date), (clockTimestamp + 1) * 1000);
assert.isUndefined(p);
done();
});
});
});

describe('option: maxAge and clockTimestamp', function () {
// { foo: 'bar', iat: 1437018582, exp: 1437018800 }
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 = 1437018582;
var options = {
algorithms: ['HS256'],
clockTimestamp: clockTimestamp,
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) {
var clockTimestamp = 1437018582;
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '600ms'};

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;
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;
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'};

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), 1437018800000);
assert.isUndefined(p);
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();
});
});
});
});
});
15 changes: 12 additions & 3 deletions verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
};
}

if (options.clockTimestamp && typeof options.clockTimestamp !== 'number') {
return done(new JsonWebTokenError('clockTimestamp must be a number'));
}

var clockTimestamp = options.clockTimestamp || Math.floor(Date.now() / 1000);

if (!jwtString){
return done(new JsonWebTokenError('jwt must be provided'));
}
Expand Down Expand Up @@ -112,7 +118,7 @@ module.exports = 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) + (options.clockTolerance || 0)) {
if (payload.nbf > clockTimestamp + (options.clockTolerance || 0)) {
return done(new NotBeforeError('jwt not active', new Date(payload.nbf * 1000)));
}
}
Expand All @@ -121,7 +127,7 @@ module.exports = 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 + (options.clockTolerance || 0)) {
if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) {
return done(new TokenExpiredError('jwt expired', new Date(payload.exp * 1000)));
}
}
Expand Down Expand Up @@ -163,7 +169,10 @@ module.exports = 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) * 1000) {
// We have to compare against either options.clockTimestamp or the currentDate _with_ millis
// to not change behaviour (version 7.2.1). Should be resolve somehow for next major.
var nowOrClockTimestamp = ((options.clockTimestamp || 0) * 1000) || Date.now();
if (nowOrClockTimestamp - (payload.iat * 1000) > maxAge + (options.clockTolerance || 0) * 1000) {
return done(new TokenExpiredError('maxAge exceeded', new Date(payload.iat * 1000 + maxAge)));
}
}
Expand Down