Skip to content

Commit 8fdc150

Browse files
mborstziluvatar
authored andcommitted
Allow user to specify now. (#274)
1 parent 7f68fe0 commit 8fdc150

File tree

3 files changed

+154
-4
lines changed

3 files changed

+154
-4
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ encoded public key for RSA and ECDSA.
117117
* `ignoreNotBefore`...
118118
* `subject`: if you want to check subject (`sub`), provide a value here
119119
* `clockTolerance`: number of seconds to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers
120+
* `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
121+
* `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)
120122

121123

122124
```js

test/verify.tests.js

+140-1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,145 @@ describe('verify', function() {
189189
});
190190
});
191191
});
192-
});
193192

193+
describe('option: clockTimestamp', function () {
194+
var clockTimestamp = 1000000000;
195+
it('should verify unexpired token relative to user-provided clockTimestamp', function (done) {
196+
var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key);
197+
jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err, p) {
198+
assert.isNull(err);
199+
done();
200+
});
201+
});
202+
it('should error on expired token relative to user-provided clockTimestamp', function (done) {
203+
var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key);
204+
jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err, p) {
205+
assert.equal(err.name, 'TokenExpiredError');
206+
assert.equal(err.message, 'jwt expired');
207+
assert.equal(err.expiredAt.constructor.name, 'Date');
208+
assert.equal(Number(err.expiredAt), (clockTimestamp + 1) * 1000);
209+
assert.isUndefined(p);
210+
done();
211+
});
212+
});
213+
it('should verify clockTimestamp is a number', function (done) {
214+
var token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key);
215+
jwt.verify(token, key, {clockTimestamp: 'notANumber'}, function (err, p) {
216+
assert.equal(err.name, 'JsonWebTokenError');
217+
assert.equal(err.message,'clockTimestamp must be a number');
218+
assert.isUndefined(p);
219+
done();
220+
});
221+
});
222+
it('should verify valid token with nbf', function (done) {
223+
var token = jwt.sign({
224+
foo: 'bar',
225+
iat: clockTimestamp,
226+
nbf: clockTimestamp + 1,
227+
exp: clockTimestamp + 2
228+
}, key);
229+
jwt.verify(token, key, {clockTimestamp: clockTimestamp + 1}, function (err, p) {
230+
assert.isNull(err);
231+
done();
232+
});
233+
});
234+
it('should error on token used before nbf', function (done) {
235+
var token = jwt.sign({
236+
foo: 'bar',
237+
iat: clockTimestamp,
238+
nbf: clockTimestamp + 1,
239+
exp: clockTimestamp + 2
240+
}, key);
241+
jwt.verify(token, key, {clockTimestamp: clockTimestamp}, function (err, p) {
242+
assert.equal(err.name, 'NotBeforeError');
243+
assert.equal(err.date.constructor.name, 'Date');
244+
assert.equal(Number(err.date), (clockTimestamp + 1) * 1000);
245+
assert.isUndefined(p);
246+
done();
247+
});
248+
});
249+
});
250+
251+
describe('option: maxAge and clockTimestamp', function () {
252+
// { foo: 'bar', iat: 1437018582, exp: 1437018800 }
253+
var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA';
254+
it('should error for claims issued before a certain timespan', function (done) {
255+
var clockTimestamp = 1437018682;
256+
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1m'};
257+
258+
jwt.verify(token, key, options, function (err, p) {
259+
assert.equal(err.name, 'TokenExpiredError');
260+
assert.equal(err.message, 'maxAge exceeded');
261+
assert.equal(err.expiredAt.constructor.name, 'Date');
262+
assert.equal(Number(err.expiredAt), 1437018642000);
263+
assert.isUndefined(p);
264+
done();
265+
});
266+
});
267+
it('should not error for claims issued before a certain timespan but still inside clockTolerance timespan', function (done) {
268+
var clockTimestamp = 1437018582;
269+
var options = {
270+
algorithms: ['HS256'],
271+
clockTimestamp: clockTimestamp,
272+
maxAge: '321ms',
273+
clockTolerance: 100
274+
};
275+
276+
jwt.verify(token, key, options, function (err, p) {
277+
assert.isNull(err);
278+
assert.equal(p.foo, 'bar');
279+
done();
280+
});
281+
});
282+
it('should not error if within maxAge timespan', function (done) {
283+
var clockTimestamp = 1437018582;
284+
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '600ms'};
285+
286+
jwt.verify(token, key, options, function (err, p) {
287+
assert.isNull(err);
288+
assert.equal(p.foo, 'bar');
289+
done();
290+
});
291+
});
292+
it('can be more restrictive than expiration', function (done) {
293+
var clockTimestamp = 1437018588;
294+
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '5s'};
295+
296+
jwt.verify(token, key, options, function (err, p) {
297+
assert.equal(err.name, 'TokenExpiredError');
298+
assert.equal(err.message, 'maxAge exceeded');
299+
assert.equal(err.expiredAt.constructor.name, 'Date');
300+
assert.equal(Number(err.expiredAt), 1437018587000);
301+
assert.isUndefined(p);
302+
done();
303+
});
304+
});
305+
it('cannot be more permissive than expiration', function (done) {
306+
var clockTimestamp = 1437018900;
307+
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'};
308+
309+
jwt.verify(token, key, options, function (err, p) {
310+
// maxAge not exceded, but still expired
311+
assert.equal(err.name, 'TokenExpiredError');
312+
assert.equal(err.message, 'jwt expired');
313+
assert.equal(err.expiredAt.constructor.name, 'Date');
314+
assert.equal(Number(err.expiredAt), 1437018800000);
315+
assert.isUndefined(p);
316+
done();
317+
});
318+
});
319+
it('should error if maxAge is specified but there is no iat claim', function (done) {
320+
var clockTimestamp = 1437018582;
321+
var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.0MBPd4Bru9-fK_HY3xmuDAc6N_embknmNuhdb9bKL_U';
322+
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1s'};
323+
324+
jwt.verify(token, key, options, function (err, p) {
325+
assert.equal(err.name, 'JsonWebTokenError');
326+
assert.equal(err.message, 'iat required when maxAge is specified');
327+
assert.isUndefined(p);
328+
done();
329+
});
330+
});
331+
});
332+
});
194333
});

verify.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
3434
};
3535
}
3636

37+
if (options.clockTimestamp && typeof options.clockTimestamp !== 'number') {
38+
return done(new JsonWebTokenError('clockTimestamp must be a number'));
39+
}
40+
41+
var clockTimestamp = options.clockTimestamp || Math.floor(Date.now() / 1000);
42+
3743
if (!jwtString){
3844
return done(new JsonWebTokenError('jwt must be provided'));
3945
}
@@ -112,7 +118,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
112118
if (typeof payload.nbf !== 'number') {
113119
return done(new JsonWebTokenError('invalid nbf value'));
114120
}
115-
if (payload.nbf > Math.floor(Date.now() / 1000) + (options.clockTolerance || 0)) {
121+
if (payload.nbf > clockTimestamp + (options.clockTolerance || 0)) {
116122
return done(new NotBeforeError('jwt not active', new Date(payload.nbf * 1000)));
117123
}
118124
}
@@ -121,7 +127,7 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
121127
if (typeof payload.exp !== 'number') {
122128
return done(new JsonWebTokenError('invalid exp value'));
123129
}
124-
if (Math.floor(Date.now() / 1000) >= payload.exp + (options.clockTolerance || 0)) {
130+
if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) {
125131
return done(new TokenExpiredError('jwt expired', new Date(payload.exp * 1000)));
126132
}
127133
}
@@ -163,7 +169,10 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
163169
if (typeof payload.iat !== 'number') {
164170
return done(new JsonWebTokenError('iat required when maxAge is specified'));
165171
}
166-
if (Date.now() - (payload.iat * 1000) > maxAge + (options.clockTolerance || 0) * 1000) {
172+
// We have to compare against either options.clockTimestamp or the currentDate _with_ millis
173+
// to not change behaviour (version 7.2.1). Should be resolve somehow for next major.
174+
var nowOrClockTimestamp = ((options.clockTimestamp || 0) * 1000) || Date.now();
175+
if (nowOrClockTimestamp - (payload.iat * 1000) > maxAge + (options.clockTolerance || 0) * 1000) {
167176
return done(new TokenExpiredError('maxAge exceeded', new Date(payload.iat * 1000 + maxAge)));
168177
}
169178
}

0 commit comments

Comments
 (0)