Skip to content

Commit cfc04a9

Browse files
authored
Merge pull request #349 from ziluvatar/fix-max-age-number-and-seconds
maxAge: Fix logic with number + use seconds instead of ms
2 parents 3305cf0 + 66a4f8b commit cfc04a9

File tree

3 files changed

+100
-71
lines changed

3 files changed

+100
-71
lines changed

Diff for: README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues
122122
* `ignoreNotBefore`...
123123
* `subject`: if you want to check subject (`sub`), provide a value here
124124
* `clockTolerance`: number of seconds to tolerate when checking the `nbf` and `exp` claims, to deal with small clock differences among different servers
125-
* `maxAge`: the maximum allowed age for tokens to still be valid. Currently it is expressed in milliseconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. **We advise against using milliseconds precision, though, since JWTs can only contain seconds. The maximum precision might be reduced to seconds in the future.**
126-
* `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)
125+
* `maxAge`: the maximum allowed age for tokens to still be valid. It is expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms). Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`.
126+
* `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons.
127127

128128

129129
```js

Diff for: test/verify.tests.js

+90-62
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ describe('verify', function() {
6868
});
6969

7070
describe('expiration', function () {
71-
// { foo: 'bar', iat: 1437018582, exp: 1437018583 }
72-
var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU4M30.NmMv7sXjM1dW0eALNXud8LoXknZ0mH14GtnFclwJv0s';
71+
// { foo: 'bar', iat: 1437018582, exp: 1437018592 }
72+
var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU';
7373
var key = 'key';
7474

7575
var clock;
@@ -78,22 +78,22 @@ describe('verify', function() {
7878
});
7979

8080
it('should error on expired token', function (done) {
81-
clock = sinon.useFakeTimers(1437018650000);
81+
clock = sinon.useFakeTimers(1437018650000); // iat + 58s, exp + 48s
8282
var options = {algorithms: ['HS256']};
8383

8484
jwt.verify(token, key, options, function (err, p) {
8585
assert.equal(err.name, 'TokenExpiredError');
8686
assert.equal(err.message, 'jwt expired');
8787
assert.equal(err.expiredAt.constructor.name, 'Date');
88-
assert.equal(Number(err.expiredAt), 1437018583000);
88+
assert.equal(Number(err.expiredAt), 1437018592000);
8989
assert.isUndefined(p);
9090
done();
9191
});
9292
});
9393

9494
it('should not error on expired token within clockTolerance interval', function (done) {
95-
clock = sinon.useFakeTimers(1437018584000);
96-
var options = {algorithms: ['HS256'], clockTolerance: 100}
95+
clock = sinon.useFakeTimers(1437018594000); // iat + 12s, exp + 2s
96+
var options = {algorithms: ['HS256'], clockTolerance: 5 }
9797

9898
jwt.verify(token, key, options, function (err, p) {
9999
assert.isNull(err);
@@ -103,8 +103,8 @@ describe('verify', function() {
103103
});
104104

105105
it('should not error if within maxAge timespan', function (done) {
106-
clock = sinon.useFakeTimers(1437018582500);
107-
var options = {algorithms: ['HS256'], maxAge: '600ms'};
106+
clock = sinon.useFakeTimers(1437018587500); // iat + 5.5s, exp - 4.5s
107+
var options = {algorithms: ['HS256'], maxAge: '6s'};
108108

109109
jwt.verify(token, key, options, function (err, p) {
110110
assert.isNull(err);
@@ -114,70 +114,97 @@ describe('verify', function() {
114114
});
115115

116116
describe('option: maxAge', function () {
117-
it('should error for claims issued before a certain timespan', function (done) {
118-
clock = sinon.useFakeTimers(1437018582500);
119-
var options = {algorithms: ['HS256'], maxAge: '321ms'};
120117

121-
jwt.verify(token, key, options, function (err, p) {
122-
assert.equal(err.name, 'TokenExpiredError');
123-
assert.equal(err.message, 'maxAge exceeded');
124-
assert.equal(err.expiredAt.constructor.name, 'Date');
125-
assert.equal(Number(err.expiredAt), 1437018582321);
126-
assert.isUndefined(p);
127-
done();
118+
[String('3s'), '3s', 3].forEach(function(maxAge) {
119+
it(`should error for claims issued before a certain timespan (${typeof maxAge} type)`, function (done) {
120+
clock = sinon.useFakeTimers(1437018587000); // iat + 5s, exp - 5s
121+
var options = {algorithms: ['HS256'], maxAge: maxAge};
122+
123+
jwt.verify(token, key, options, function (err, p) {
124+
assert.equal(err.name, 'TokenExpiredError');
125+
assert.equal(err.message, 'maxAge exceeded');
126+
assert.equal(err.expiredAt.constructor.name, 'Date');
127+
assert.equal(Number(err.expiredAt), 1437018585000);
128+
assert.isUndefined(p);
129+
done();
130+
});
128131
});
129132
});
130133

131-
it('should not error for claims issued before a certain timespan but still inside clockTolerance timespan', function (done) {
132-
clock = sinon.useFakeTimers(1437018582500);
133-
var options = {algorithms: ['HS256'], maxAge: '321ms', clockTolerance: 100};
134+
[String('5s'), '5s', 5].forEach(function (maxAge) {
135+
it(`should not error for claims issued before a certain timespan but still inside clockTolerance timespan (${typeof maxAge} type)`, function (done) {
136+
clock = sinon.useFakeTimers(1437018587500); // iat + 5.5s, exp - 4.5s
137+
var options = {algorithms: ['HS256'], maxAge: maxAge, clockTolerance: 1 };
134138

135-
jwt.verify(token, key, options, function (err, p) {
136-
assert.isNull(err);
137-
assert.equal(p.foo, 'bar');
138-
done();
139+
jwt.verify(token, key, options, function (err, p) {
140+
assert.isNull(err);
141+
assert.equal(p.foo, 'bar');
142+
done();
143+
});
139144
});
140145
});
141146

142-
it('should not error if within maxAge timespan', function (done) {
143-
clock = sinon.useFakeTimers(1437018582500);
144-
var options = {algorithms: ['HS256'], maxAge: '600ms'};
147+
[String('6s'), '6s', 6].forEach(function (maxAge) {
148+
it(`should not error if within maxAge timespan (${typeof maxAge} type)`, function (done) {
149+
clock = sinon.useFakeTimers(1437018587500);// iat + 5.5s, exp - 4.5s
150+
var options = {algorithms: ['HS256'], maxAge: maxAge};
145151

146-
jwt.verify(token, key, options, function (err, p) {
147-
assert.isNull(err);
148-
assert.equal(p.foo, 'bar');
149-
done();
152+
jwt.verify(token, key, options, function (err, p) {
153+
assert.isNull(err);
154+
assert.equal(p.foo, 'bar');
155+
done();
156+
});
150157
});
151158
});
152-
it('can be more restrictive than expiration', function (done) {
153-
clock = sinon.useFakeTimers(1437018582900);
154-
var options = {algorithms: ['HS256'], maxAge: '800ms'};
155159

156-
jwt.verify(token, key, options, function (err, p) {
157-
assert.equal(err.name, 'TokenExpiredError');
158-
assert.equal(err.message, 'maxAge exceeded');
159-
assert.equal(err.expiredAt.constructor.name, 'Date');
160-
assert.equal(Number(err.expiredAt), 1437018582800);
161-
assert.isUndefined(p);
162-
done();
160+
[String('8s'), '8s', 8].forEach(function (maxAge) {
161+
it(`can be more restrictive than expiration (${typeof maxAge} type)`, function (done) {
162+
clock = sinon.useFakeTimers(1437018591900); // iat + 9.9s, exp - 0.1s
163+
var options = {algorithms: ['HS256'], maxAge: maxAge };
164+
165+
jwt.verify(token, key, options, function (err, p) {
166+
assert.equal(err.name, 'TokenExpiredError');
167+
assert.equal(err.message, 'maxAge exceeded');
168+
assert.equal(err.expiredAt.constructor.name, 'Date');
169+
assert.equal(Number(err.expiredAt), 1437018590000);
170+
assert.isUndefined(p);
171+
done();
172+
});
163173
});
164174
});
165-
it('cannot be more permissive than expiration', function (done) {
166-
clock = sinon.useFakeTimers(1437018583100);
167-
var options = {algorithms: ['HS256'], maxAge: '1200ms'};
168175

169-
jwt.verify(token, key, options, function (err, p) {
170-
// maxAge not exceded, but still expired
171-
assert.equal(err.name, 'TokenExpiredError');
172-
assert.equal(err.message, 'jwt expired');
173-
assert.equal(err.expiredAt.constructor.name, 'Date');
174-
assert.equal(Number(err.expiredAt), 1437018583000);
175-
assert.isUndefined(p);
176-
done();
176+
[String('12s'), '12s', 12].forEach(function (maxAge) {
177+
it(`cannot be more permissive than expiration (${typeof maxAge} type)`, function (done) {
178+
clock = sinon.useFakeTimers(1437018593000); // iat + 11s, exp + 1s
179+
var options = {algorithms: ['HS256'], maxAge: '12s'};
180+
181+
jwt.verify(token, key, options, function (err, p) {
182+
// maxAge not exceded, but still expired
183+
assert.equal(err.name, 'TokenExpiredError');
184+
assert.equal(err.message, 'jwt expired');
185+
assert.equal(err.expiredAt.constructor.name, 'Date');
186+
assert.equal(Number(err.expiredAt), 1437018592000);
187+
assert.isUndefined(p);
188+
done();
189+
});
190+
});
191+
});
192+
193+
[new String('1s'), 'no-timespan-string'].forEach(function (maxAge){
194+
it(`should error if maxAge is specified with a wrong string format/type (value: ${maxAge}, type: ${typeof maxAge})`, function (done) {
195+
clock = sinon.useFakeTimers(1437018587000); // iat + 5s, exp - 5s
196+
var options = { algorithms: ['HS256'], maxAge: maxAge };
197+
198+
jwt.verify(token, key, options, function (err, p) {
199+
assert.equal(err.name, 'JsonWebTokenError');
200+
assert.equal(err.message, '"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60');
201+
assert.isUndefined(p);
202+
done();
203+
});
177204
});
178205
});
206+
179207
it('should error if maxAge is specified but there is no iat claim', function (done) {
180-
clock = sinon.useFakeTimers(1437018582900);
181208
var token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.0MBPd4Bru9-fK_HY3xmuDAc6N_embknmNuhdb9bKL_U';
182209
var options = {algorithms: ['HS256'], maxAge: '1s'};
183210

@@ -188,6 +215,7 @@ describe('verify', function() {
188215
done();
189216
});
190217
});
218+
191219
});
192220

193221
describe('option: clockTimestamp', function () {
@@ -249,7 +277,7 @@ describe('verify', function() {
249277
});
250278

251279
describe('option: maxAge and clockTimestamp', function () {
252-
// { foo: 'bar', iat: 1437018582, exp: 1437018800 }
280+
// { foo: 'bar', iat: 1437018582, exp: 1437018800 } exp = iat + 218s
253281
var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA';
254282
it('should error for claims issued before a certain timespan', function (done) {
255283
var clockTimestamp = 1437018682;
@@ -265,12 +293,12 @@ describe('verify', function() {
265293
});
266294
});
267295
it('should not error for claims issued before a certain timespan but still inside clockTolerance timespan', function (done) {
268-
var clockTimestamp = 1437018582;
296+
var clockTimestamp = 1437018592; // iat + 10s
269297
var options = {
270298
algorithms: ['HS256'],
271299
clockTimestamp: clockTimestamp,
272-
maxAge: '321ms',
273-
clockTolerance: 100
300+
maxAge: '3s',
301+
clockTolerance: 10
274302
};
275303

276304
jwt.verify(token, key, options, function (err, p) {
@@ -280,8 +308,8 @@ describe('verify', function() {
280308
});
281309
});
282310
it('should not error if within maxAge timespan', function (done) {
283-
var clockTimestamp = 1437018582;
284-
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '600ms'};
311+
var clockTimestamp = 1437018587; // iat + 5s
312+
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '6s'};
285313

286314
jwt.verify(token, key, options, function (err, p) {
287315
assert.isNull(err);
@@ -290,7 +318,7 @@ describe('verify', function() {
290318
});
291319
});
292320
it('can be more restrictive than expiration', function (done) {
293-
var clockTimestamp = 1437018588;
321+
var clockTimestamp = 1437018588; // iat + 6s
294322
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '5s'};
295323

296324
jwt.verify(token, key, options, function (err, p) {
@@ -303,7 +331,7 @@ describe('verify', function() {
303331
});
304332
});
305333
it('cannot be more permissive than expiration', function (done) {
306-
var clockTimestamp = 1437018900;
334+
var clockTimestamp = 1437018900; // iat + 318s (exp: iat + 218s)
307335
var options = {algorithms: ['HS256'], clockTimestamp: clockTimestamp, maxAge: '1000y'};
308336

309337
jwt.verify(token, key, options, function (err, p) {

Diff for: verify.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ var JsonWebTokenError = require('./lib/JsonWebTokenError');
22
var NotBeforeError = require('./lib/NotBeforeError');
33
var TokenExpiredError = require('./lib/TokenExpiredError');
44
var decode = require('./decode');
5+
var timespan = require('./lib/timespan');
56
var jws = require('jws');
6-
var ms = require('ms');
77
var xtend = require('xtend');
88

99
module.exports = function (jwtString, secretOrPublicKey, options, callback) {
@@ -160,15 +160,16 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
160160
}
161161

162162
if (options.maxAge) {
163-
var maxAge = ms(options.maxAge);
164163
if (typeof payload.iat !== 'number') {
165164
return done(new JsonWebTokenError('iat required when maxAge is specified'));
166165
}
167-
// We have to compare against either options.clockTimestamp or the currentDate _with_ millis
168-
// to not change behaviour (version 7.2.1). Should be resolve somehow for next major.
169-
var nowOrClockTimestamp = ((options.clockTimestamp || 0) * 1000) || Date.now();
170-
if (nowOrClockTimestamp - (payload.iat * 1000) > maxAge + (options.clockTolerance || 0) * 1000) {
171-
return done(new TokenExpiredError('maxAge exceeded', new Date(payload.iat * 1000 + maxAge)));
166+
167+
var maxAgeTimestamp = timespan(options.maxAge, payload.iat);
168+
if (typeof maxAgeTimestamp === 'undefined') {
169+
return done(new JsonWebTokenError('"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
170+
}
171+
if (clockTimestamp >= maxAgeTimestamp + (options.clockTolerance || 0)) {
172+
return done(new TokenExpiredError('maxAge exceeded', new Date(maxAgeTimestamp * 1000)));
172173
}
173174
}
174175

0 commit comments

Comments
 (0)