Skip to content
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

Added API endpoint for token refresh #172

Closed
wants to merge 15 commits into from
Closed
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,28 @@ console.log(decoded.header);
console.log(decoded.payload)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good

```

### 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐⭐⭐⭐⭐


* `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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be var refreshed = jwt.refresh(**token**, 3600, secret);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the point of this. You have to decode and validate it, and you will have the payload, why not just call sign on that payload instead of having a refresh function that will decode it again?


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
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
decode: require('./decode'),
verify: require('./verify'),
sign: require('./sign'),
refresh: require('./refresh'),
JsonWebTokenError: require('./lib/JsonWebTokenError'),
NotBeforeError: require('./lib/NotBeforeError'),
TokenExpiredError: require('./lib/TokenExpiredError'),
Expand Down
114 changes: 114 additions & 0 deletions refresh.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
var sign = require('./sign');
var verify = require('./verify');
var decode = require('./decode');

/**
* 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 verifyOptions - Options to verify the token
* @param callback
* @return New signed JWT token
*/
module.exports = function(token, expiresIn, secretOrPrivateKey, verifyOptions, 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 verified;
var header;
var payload;
var decoded = decode(token, {complete: true});

try {
verified = verify(token, secretOrPrivateKey, verifyOptions);
}
catch (error) {
verified = null;
}

if (verified) {
if (decoded.header) {
header = decoded['header'];
payload = decoded['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.header = { };
for (var key in header) {
if (key !== 'typ') { //don't care about typ -> always JWT
if (Object.keys(optionMapping).indexOf(key) === -1) {
options.header[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 = sign(obj, secretOrPrivateKey, options);
return done(null, newToken);
}
else {
return done('Token invalid. Failed to verify.');
}
};
152 changes: 152 additions & 0 deletions test/refresh.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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 = function (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',
header: {
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(token, 3600, secret);
assert.ok(refreshed);
done();
});

it('Should be able to refresh the token (async)', function (done) {

jwt.refresh(token, 3600, secret, null, function(err, refreshedToken) {

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

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

refreshAsync = refreshedToken;
refreshAsyncDecoded = jwt.decode(refreshedToken, {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(refreshAsyncDecoded, 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();
});
});