Skip to content

Commit 6d3250d

Browse files
author
Jean-Philipe Pellerin
committed
According to issue auth0#122, there was some interest in having an endpoint to refresh a token. A refresh is considered to have the same token returned, but with a later expiry time.
Can take into account the header + payload of a decoded token ie : {complete: true} More in depth testing with comparision of equality Testing failures and async mode Added description for refresh in README.md
1 parent bd82ab3 commit 6d3250d

File tree

3 files changed

+292
-0
lines changed

3 files changed

+292
-0
lines changed

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,28 @@ console.log(decoded.header);
169169
console.log(decoded.payload)
170170
```
171171

172+
### jwt.refresh(token, expiresIn, secretOrPrivateKey [, callback])
173+
174+
Will refresh the given token. The token is __expected__ to be *decoded* and *valid*. No checks will be performed on the token. The function will copy the values of the token, give it a new expiry time based on the given `expiresIn` parameter and will return a new signed token using the `sign` function and given secretOrPrivateKey.
175+
176+
* `token`: is the *decoded* JsonWebToken string
177+
* `expiresIn` : New value to set when the token will expire.
178+
* `secretOrPrivateKey` : is a string or buffer containing either the secret for HMAC algorithms, or the PEM
179+
encoded private key for RSA and ECDSA.
180+
* `callback` : If a callback is supplied, callback is called with the newly refreshed JsonWebToken string
181+
182+
Example
183+
184+
```js
185+
// ...
186+
var originalDecoded = jwt.decode(token, {complete: true});
187+
var refreshed = jwt.refresh(originalDecoded, 3600, secret);
188+
189+
console.log(JSON.stringify(originalDecoded));
190+
// new 'exp' value is later in the future.
191+
console.log(JSON.stringify(jwt.decode(refreshed, {complete: true})));
192+
```
193+
172194
## Errors & Codes
173195
Possible thrown errors during verification.
174196
Error is the first argument of the verification callback.

index.js

+93
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,96 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) {
270270

271271
return done(null, payload);
272272
};
273+
274+
/**
275+
* Will refresh the given token. The token is expected to be decoded and valid. No checks will be
276+
* performed on the token. The function will copy the values of the token, give it a new
277+
* expiry time based on the given 'expiresIn' time and will return a new signed token.
278+
*
279+
* @param token
280+
* @param expiresIn
281+
* @param secretOrPrivateKey
282+
* @param callback
283+
* @return New signed JWT token
284+
*/
285+
JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) {
286+
//TODO: check if token is not good, if so return error ie: no payload, not required fields, etc.
287+
288+
var done;
289+
if (callback) {
290+
done = function() {
291+
var args = Array.prototype.slice.call(arguments, 0);
292+
return process.nextTick(function() {
293+
callback.apply(null, args);
294+
});
295+
};
296+
}
297+
else {
298+
done = function(err, data) {
299+
if (err) {
300+
console.log('err : ' + err);
301+
throw err;
302+
}
303+
return data;
304+
};
305+
}
306+
307+
var header;
308+
var payload;
309+
310+
if (token.header) {
311+
header = token['header'];
312+
payload = token['payload'];
313+
}
314+
else {
315+
payload = token;
316+
}
317+
318+
var optionMapping = {
319+
exp: 'expiresIn',
320+
aud: 'audience',
321+
nbf: 'notBefore',
322+
iss: 'issuer',
323+
sub: 'subject',
324+
jti: 'jwtid',
325+
alg: 'algorithm'
326+
};
327+
var newToken;
328+
var obj = {};
329+
var options = {};
330+
331+
for (var key in payload) {
332+
if (Object.keys(optionMapping).indexOf(key) === -1) {
333+
obj[key] = payload[key];
334+
}
335+
else {
336+
options[optionMapping[key]] = payload[key];
337+
}
338+
}
339+
340+
if(header) {
341+
options.headers = { };
342+
for (var key in header) {
343+
if (key !== 'typ') { //don't care about typ -> always JWT
344+
if (Object.keys(optionMapping).indexOf(key) === -1) {
345+
options.headers[key] = header[key];
346+
}
347+
else {
348+
options[optionMapping[key]] = header[key];
349+
}
350+
}
351+
}
352+
}
353+
else {
354+
console.log('No algorithm was defined for token refresh - using default');
355+
}
356+
357+
if (!token.iat) {
358+
options['noTimestamp'] = true;
359+
}
360+
361+
options['expiresIn'] = expiresIn;
362+
363+
newToken = JWT.sign(obj, secretOrPrivateKey, options);
364+
return done(null, newToken);
365+
};

test/refresh.tests.js

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
var jwt = require('../index');
2+
var jws = require('jws');
3+
var fs = require('fs');
4+
var path = require('path');
5+
var sinon = require('sinon');
6+
7+
var assert = require('chai').assert;
8+
9+
/**
10+
* Method to verify if first token is euqal to second token. This is a symmetric
11+
* test. Will check that first = second, and that second = first.
12+
*
13+
* All properties are tested, except for the 'iat' and 'exp' values since we do not
14+
* care for those as we are expecting them to be different.
15+
*
16+
* @param first - The first decoded token
17+
* @param second - The second decoded token
18+
* @param last - boolean value to state that this is the last test and no need to rerun
19+
* the symmetric test.
20+
* @return boolean - true if the tokens match.
21+
*/
22+
var equal = (first, second, last) => {
23+
var noCompare = ['iat', 'exp'];
24+
var areEqual = true;
25+
26+
if (first.header) {
27+
var equalHeader = equal(first.header, second.header);
28+
var equalPayload = equal(first.payload, second.payload);
29+
areEqual = (equalHeader && equalPayload);
30+
}
31+
else {
32+
for (var key in first) {
33+
if (noCompare.indexOf(key) === -1) {
34+
if (first[key] !== second[key]) {
35+
areEqual = false;
36+
break;
37+
}
38+
}
39+
else {
40+
//not caring about iat and exp
41+
}
42+
}
43+
}
44+
45+
if (!last) {
46+
areEqual = equal(second, first, true);
47+
}
48+
49+
return areEqual;
50+
}
51+
52+
describe('Refresh Token Testing', function() {
53+
54+
var secret = 'ssshhhh';
55+
var options = {
56+
algorithm: 'HS256',
57+
expiresIn: '3600',
58+
subject: 'Testing Refresh',
59+
issuer: 'node-jsonwebtoken',
60+
headers: {
61+
a: 'header'
62+
}
63+
};
64+
var payload = {
65+
scope: 'admin',
66+
something: 'else',
67+
more: 'payload'
68+
};
69+
70+
var expectedPayloadNoHeader = {
71+
scope: 'admin',
72+
something: 'else',
73+
more: 'payload',
74+
expiresIn: '3600',
75+
subject: 'Testing Refresh',
76+
issuer: 'node-jsonwebtoken'
77+
}
78+
79+
var token = jwt.sign(payload, secret, options);
80+
81+
it('Should be able to verify token normally', function (done) {
82+
jwt.verify(token, secret, {typ: 'JWT'}, function(err, p) {
83+
assert.isNull(err);
84+
done();
85+
});
86+
});
87+
88+
it('Should be able to decode the token (proof of good token)', function (done) {
89+
var decoded = jwt.decode(token, {complete: true});
90+
assert.ok(decoded.payload.scope);
91+
assert.equal('admin', decoded.payload.scope);
92+
done();
93+
});
94+
95+
it('Should be able to refresh the token', function (done) {
96+
var refreshed = jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret);
97+
assert.ok(refreshed);
98+
done();
99+
});
100+
101+
it('Should be able to refresh the token (async)', function (done) {
102+
var refreshed = jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret, function(err, refreshedToken) {
103+
assert.ok(refreshedToken);
104+
});
105+
done();
106+
});
107+
108+
var originalDecoded = jwt.decode(token, {complete: true});
109+
var refreshed = jwt.refresh(originalDecoded, 3600, secret);
110+
var refreshDecoded = jwt.decode(refreshed, {complete: true});
111+
var refreshAsync;
112+
var refreshAsyncDecoded;
113+
jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret, function(err, refreshedToken) {
114+
refreshAsync = refreshedToken;
115+
refreshAsyncDecoded = jwt.decode(refreshed, {complete: true});
116+
});
117+
118+
it('Sub-test to ensure that the compare method works', function (done) {
119+
var originalMatch = equal(originalDecoded, originalDecoded);
120+
var refreshMatch = equal(refreshDecoded, refreshDecoded);
121+
var asyncRefreshMatch = equal(originalDecoded, refreshAsyncDecoded);
122+
123+
assert.equal(originalMatch, refreshMatch);
124+
assert.equal(originalMatch, asyncRefreshMatch);
125+
done();
126+
});
127+
128+
it('Decoded version of a refreshed token should be the same, except for timing data', function (done) {
129+
var comparison = equal(originalDecoded, refreshDecoded);
130+
var asyncComparison = equal(originalDecoded, refreshAsyncDecoded);
131+
132+
assert.ok(comparison);
133+
assert.ok(asyncComparison);
134+
done();
135+
});
136+
137+
it('Refreshed token should have a later expiery time then the original', function (done) {
138+
var originalExpiry = originalDecoded.payload.exp;
139+
var refreshedExpiry = refreshDecoded.payload.exp;
140+
var refreshedAsyncExpiry = refreshAsyncDecoded.payload.exp;
141+
142+
assert.isTrue((refreshedExpiry > originalExpiry), 'Refreshed expiry time is above original time');
143+
assert.isTrue((refreshedAsyncExpiry > originalExpiry), 'Refreshed expiry time is above original time (async)');
144+
done();
145+
});
146+
147+
it('Refreshing a token that\'s is not from an original decoded token should still work - basically creating a brand new token', function (done) {
148+
var notReallyAToken = {
149+
key: 'value',
150+
foo: 'bar',
151+
not: 'a token'
152+
}
153+
var notReallyATokenRefresh = jwt.refresh(notReallyAToken, 3600, secret);
154+
155+
assert.ok(notReallyATokenRefresh);
156+
done();
157+
});
158+
159+
it('Should fail when not providing a time value for the expiresIn value', function (done) {
160+
var notReallyAToken = {
161+
key: 'value',
162+
foo: 'bar',
163+
not: 'a token'
164+
}
165+
166+
var failRefresh;
167+
try {
168+
var failRefresh = jwt.refresh(notReallyAToken, null, secret);
169+
} catch (err) {
170+
assert.equal(err.name, 'Error');
171+
assert.equal(err.message, '"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60');
172+
}
173+
174+
assert.notOk(failRefresh);
175+
done();
176+
});
177+
});

0 commit comments

Comments
 (0)