Skip to content

Commit

Permalink
According to issue auth0#122, there was some interest in having an en…
Browse files Browse the repository at this point in the history
…dpoint 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
  • Loading branch information
Jean-Philipe Pellerin committed Feb 3, 2016
1 parent bd82ab3 commit 6d3250d
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 0 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,28 @@ console.log(decoded.header);
console.log(decoded.payload)
```

### jwt.refresh(token, expiresIn, secretOrPrivateKey [, callback])

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.

* `token`: is the *decoded* JsonWebToken string
* `expiresIn` : New value to set when the token will expire.
* `secretOrPrivateKey` : is a string or buffer containing either the secret for HMAC algorithms, or the PEM
encoded private key for RSA and ECDSA.
* `callback` : If a callback is supplied, callback is called with the newly refreshed JsonWebToken string

Example

```js
// ...
var originalDecoded = jwt.decode(token, {complete: true});
var refreshed = jwt.refresh(originalDecoded, 3600, secret);

console.log(JSON.stringify(originalDecoded));
// new 'exp' value is later in the future.
console.log(JSON.stringify(jwt.decode(refreshed, {complete: true})));
```

## Errors & Codes
Possible thrown errors during verification.
Error is the first argument of the verification callback.
Expand Down
93 changes: 93 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,96 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) {

return done(null, payload);
};

/**
* 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' time and will return a new signed token.
*
* @param token
* @param expiresIn
* @param secretOrPrivateKey
* @param callback
* @return New signed JWT token
*/
JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) {
//TODO: check if token is not good, if so return error ie: no payload, not required fields, etc.

var done;
if (callback) {
done = function() {
var args = Array.prototype.slice.call(arguments, 0);
return process.nextTick(function() {
callback.apply(null, args);
});
};
}
else {
done = function(err, data) {
if (err) {
console.log('err : ' + err);
throw err;
}
return data;
};
}

var header;
var payload;

if (token.header) {
header = token['header'];
payload = token['payload'];
}
else {
payload = token;
}

var optionMapping = {
exp: 'expiresIn',
aud: 'audience',
nbf: 'notBefore',
iss: 'issuer',
sub: 'subject',
jti: 'jwtid',
alg: 'algorithm'
};
var newToken;
var obj = {};
var options = {};

for (var key in payload) {
if (Object.keys(optionMapping).indexOf(key) === -1) {
obj[key] = payload[key];
}
else {
options[optionMapping[key]] = payload[key];
}
}

if(header) {
options.headers = { };
for (var key in header) {
if (key !== 'typ') { //don't care about typ -> always JWT
if (Object.keys(optionMapping).indexOf(key) === -1) {
options.headers[key] = header[key];
}
else {
options[optionMapping[key]] = header[key];
}
}
}
}
else {
console.log('No algorithm was defined for token refresh - using default');
}

if (!token.iat) {
options['noTimestamp'] = true;
}

options['expiresIn'] = expiresIn;

newToken = JWT.sign(obj, secretOrPrivateKey, options);
return done(null, newToken);
};
177 changes: 177 additions & 0 deletions test/refresh.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
var jwt = require('../index');
var jws = require('jws');
var fs = require('fs');
var path = require('path');
var sinon = require('sinon');

var assert = require('chai').assert;

/**
* Method to verify if first token is euqal to second token. This is a symmetric
* test. Will check that first = second, and that second = first.
*
* All properties are tested, except for the 'iat' and 'exp' values since we do not
* care for those as we are expecting them to be different.
*
* @param first - The first decoded token
* @param second - The second decoded token
* @param last - boolean value to state that this is the last test and no need to rerun
* the symmetric test.
* @return boolean - true if the tokens match.
*/
var equal = (first, second, last) => {
var noCompare = ['iat', 'exp'];
var areEqual = true;

if (first.header) {
var equalHeader = equal(first.header, second.header);
var equalPayload = equal(first.payload, second.payload);
areEqual = (equalHeader && equalPayload);
}
else {
for (var key in first) {
if (noCompare.indexOf(key) === -1) {
if (first[key] !== second[key]) {
areEqual = false;
break;
}
}
else {
//not caring about iat and exp
}
}
}

if (!last) {
areEqual = equal(second, first, true);
}

return areEqual;
}

describe('Refresh Token Testing', function() {

var secret = 'ssshhhh';
var options = {
algorithm: 'HS256',
expiresIn: '3600',
subject: 'Testing Refresh',
issuer: 'node-jsonwebtoken',
headers: {
a: 'header'
}
};
var payload = {
scope: 'admin',
something: 'else',
more: 'payload'
};

var expectedPayloadNoHeader = {
scope: 'admin',
something: 'else',
more: 'payload',
expiresIn: '3600',
subject: 'Testing Refresh',
issuer: 'node-jsonwebtoken'
}

var token = jwt.sign(payload, secret, options);

it('Should be able to verify token normally', function (done) {
jwt.verify(token, secret, {typ: 'JWT'}, function(err, p) {
assert.isNull(err);
done();
});
});

it('Should be able to decode the token (proof of good token)', function (done) {
var decoded = jwt.decode(token, {complete: true});
assert.ok(decoded.payload.scope);
assert.equal('admin', decoded.payload.scope);
done();
});

it('Should be able to refresh the token', function (done) {
var refreshed = jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret);
assert.ok(refreshed);
done();
});

it('Should be able to refresh the token (async)', function (done) {
var refreshed = jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret, function(err, refreshedToken) {
assert.ok(refreshedToken);
});
done();
});

var originalDecoded = jwt.decode(token, {complete: true});
var refreshed = jwt.refresh(originalDecoded, 3600, secret);
var refreshDecoded = jwt.decode(refreshed, {complete: true});
var refreshAsync;
var refreshAsyncDecoded;
jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret, function(err, refreshedToken) {
refreshAsync = refreshedToken;
refreshAsyncDecoded = jwt.decode(refreshed, {complete: true});
});

it('Sub-test to ensure that the compare method works', function (done) {
var originalMatch = equal(originalDecoded, originalDecoded);
var refreshMatch = equal(refreshDecoded, refreshDecoded);
var asyncRefreshMatch = equal(originalDecoded, refreshAsyncDecoded);

assert.equal(originalMatch, refreshMatch);
assert.equal(originalMatch, asyncRefreshMatch);
done();
});

it('Decoded version of a refreshed token should be the same, except for timing data', function (done) {
var comparison = equal(originalDecoded, refreshDecoded);
var asyncComparison = equal(originalDecoded, refreshAsyncDecoded);

assert.ok(comparison);
assert.ok(asyncComparison);
done();
});

it('Refreshed token should have a later expiery time then the original', function (done) {
var originalExpiry = originalDecoded.payload.exp;
var refreshedExpiry = refreshDecoded.payload.exp;
var refreshedAsyncExpiry = refreshAsyncDecoded.payload.exp;

assert.isTrue((refreshedExpiry > originalExpiry), 'Refreshed expiry time is above original time');
assert.isTrue((refreshedAsyncExpiry > originalExpiry), 'Refreshed expiry time is above original time (async)');
done();
});

it('Refreshing a token that\'s is not from an original decoded token should still work - basically creating a brand new token', function (done) {
var notReallyAToken = {
key: 'value',
foo: 'bar',
not: 'a token'
}
var notReallyATokenRefresh = jwt.refresh(notReallyAToken, 3600, secret);

assert.ok(notReallyATokenRefresh);
done();
});

it('Should fail when not providing a time value for the expiresIn value', function (done) {
var notReallyAToken = {
key: 'value',
foo: 'bar',
not: 'a token'
}

var failRefresh;
try {
var failRefresh = jwt.refresh(notReallyAToken, null, secret);
} catch (err) {
assert.equal(err.name, 'Error');
assert.equal(err.message, '"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60');
}

assert.notOk(failRefresh);
done();
});
});

0 comments on commit 6d3250d

Please sign in to comment.