From bebaf16672a0d59e934322d60dedfff8bc7d7a23 Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Tue, 2 Feb 2016 09:21:26 -0500 Subject: [PATCH 01/11] Initial work done. Writing basic tests. Need to look at different cases --- index.js | 37 +++++++++++++++++++++++++++ test/refresh.tests.js | 58 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 test/refresh.tests.js diff --git a/index.js b/index.js index 221dba2..7285adb 100644 --- a/index.js +++ b/index.js @@ -270,3 +270,40 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) { return done(null, payload); }; + +/** +* +*/ +JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { + 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 token) { + if (Object.keys(optionMapping).indexOf(key) === -1) { + obj[key] = token[key]; + } + else { + options[optionMapping[key]] = token[key]; + } + } + + if (!token.iat) { + options['noTimestamp'] = true; + } + + options['expiresIn'] = expiresIn; + + newToken = JWT.sign(obj, secretOrPrivateKey, options); + return newToken; + //callback(null, newToken); +}; diff --git a/test/refresh.tests.js b/test/refresh.tests.js new file mode 100644 index 0000000..063dfd5 --- /dev/null +++ b/test/refresh.tests.js @@ -0,0 +1,58 @@ +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; + +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(token, 3600, secret); + console.log(JSON.stringify(refreshed)); + assert.ok(refreshed); + done(); + }); +}); From 10f2f9f0e82ff9c5d69674762779e5c1c33dd058 Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Tue, 2 Feb 2016 09:29:48 -0500 Subject: [PATCH 02/11] Refresh works, but only when just the payload is decoded, not when the {complete: true} option is on --- index.js | 6 ++++++ test/refresh.tests.js | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 7285adb..aa3ba15 100644 --- a/index.js +++ b/index.js @@ -275,6 +275,9 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) { * */ JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { + //TODO: if token is {complete: true}, then need to get header, payload and signature, + // if not, then we're just getting the payload + var optionMapping = { exp: 'expiresIn', aud: 'audience', @@ -289,6 +292,7 @@ JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { var options = {}; for (var key in token) { + console.log('key : ' + key + ' -- ' + Object.keys(optionMapping)); if (Object.keys(optionMapping).indexOf(key) === -1) { obj[key] = token[key]; } @@ -297,6 +301,8 @@ JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { } } + console.log('options: ' + JSON.stringify(options)); + if (!token.iat) { options['noTimestamp'] = true; } diff --git a/test/refresh.tests.js b/test/refresh.tests.js index 063dfd5..7134931 100644 --- a/test/refresh.tests.js +++ b/test/refresh.tests.js @@ -50,9 +50,17 @@ describe('Refresh Token Testing', function() { }); it('Should be able to refresh the token', function (done) { - var refreshed = jwt.refresh(token, 3600, secret); - console.log(JSON.stringify(refreshed)); + var refreshed = jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret); + // console.log(JSON.stringify(refreshed)); assert.ok(refreshed); done(); }); + + it('Decoded version of a refreshed token should be the same, except for timing data', function (done) { + var refreshed = jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret); + var decoded = jwt.decode(refreshed, {complete: true}); + // console.log(JSON.stringify(decoded)); + assert.ok(decoded); + done(); + }); }); From 8c81d3495d0eec10fbce27c6b1d13c3ca4a16b58 Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Tue, 2 Feb 2016 17:37:35 -0500 Subject: [PATCH 03/11] Can take into account the header + payload of a decoded token ie : {complete: true} --- index.js | 40 +++++++++++++++++++++++++++++++------ test/refresh.tests.js | 46 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index aa3ba15..c42b799 100644 --- a/index.js +++ b/index.js @@ -275,8 +275,20 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) { * */ JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { - //TODO: if token is {complete: true}, then need to get header, payload and signature, - // if not, then we're just getting the payload + + var header; + var payload; + + if (token.header) { + header = token['header']; + payload = token['payload']; + } + else { + payload = token; + } + + console.log('header: ' + JSON.stringify(header)); + console.log('payload: ' + JSON.stringify(payload)); var optionMapping = { exp: 'expiresIn', @@ -291,16 +303,32 @@ JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { var obj = {}; var options = {}; - for (var key in token) { - console.log('key : ' + key + ' -- ' + Object.keys(optionMapping)); + for (var key in payload) { if (Object.keys(optionMapping).indexOf(key) === -1) { - obj[key] = token[key]; + obj[key] = payload[key]; } else { - options[optionMapping[key]] = token[key]; + 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'); + } + console.log('options: ' + JSON.stringify(options)); if (!token.iat) { diff --git a/test/refresh.tests.js b/test/refresh.tests.js index 7134931..c04d732 100644 --- a/test/refresh.tests.js +++ b/test/refresh.tests.js @@ -6,6 +6,37 @@ var sinon = require('sinon'); var assert = require('chai').assert; +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) { + console.log(key + ' -> ' + first[key] + ' : ' + second[key]); + 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'; @@ -57,10 +88,17 @@ describe('Refresh Token Testing', function() { }); it('Decoded version of a refreshed token should be the same, except for timing data', function (done) { - var refreshed = jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret); - var decoded = jwt.decode(refreshed, {complete: true}); - // console.log(JSON.stringify(decoded)); - assert.ok(decoded); + var originalDecoded = jwt.decode(token, {complete: true}); + var refreshed = jwt.refresh(originalDecoded, 3600, secret); + var refreshDecoded = jwt.decode(refreshed, {complete: true}); + + var comparison = equal(originalDecoded, refreshDecoded); + console.log('comparison : ' + comparison); + + assert.ok(comparison); + assert.ok(refreshDecoded); + + //test that the refreshed token has a time in the future. done(); }); }); From 5d8d2885f12a879907c9ebdde0cd0db06abe530c Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Wed, 3 Feb 2016 10:11:31 -0500 Subject: [PATCH 04/11] More in depth testing with comparision of equality --- index.js | 5 ----- test/refresh.tests.js | 41 ++++++++++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index c42b799..9765e11 100644 --- a/index.js +++ b/index.js @@ -287,9 +287,6 @@ JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { payload = token; } - console.log('header: ' + JSON.stringify(header)); - console.log('payload: ' + JSON.stringify(payload)); - var optionMapping = { exp: 'expiresIn', aud: 'audience', @@ -329,8 +326,6 @@ JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { console.log('No algorithm was defined for token refresh - using default'); } - console.log('options: ' + JSON.stringify(options)); - if (!token.iat) { options['noTimestamp'] = true; } diff --git a/test/refresh.tests.js b/test/refresh.tests.js index c04d732..c23e00c 100644 --- a/test/refresh.tests.js +++ b/test/refresh.tests.js @@ -6,6 +6,19 @@ 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; @@ -18,7 +31,6 @@ var equal = (first, second, last) => { else { for (var key in first) { if (noCompare.indexOf(key) === -1) { - console.log(key + ' -> ' + first[key] + ' : ' + second[key]); if (first[key] !== second[key]) { areEqual = false; break; @@ -82,23 +94,34 @@ describe('Refresh Token Testing', function() { it('Should be able to refresh the token', function (done) { var refreshed = jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret); - // console.log(JSON.stringify(refreshed)); assert.ok(refreshed); done(); }); - it('Decoded version of a refreshed token should be the same, except for timing data', function (done) { - var originalDecoded = jwt.decode(token, {complete: true}); - var refreshed = jwt.refresh(originalDecoded, 3600, secret); - var refreshDecoded = jwt.decode(refreshed, {complete: true}); + var originalDecoded = jwt.decode(token, {complete: true}); + var refreshed = jwt.refresh(originalDecoded, 3600, secret); + var refreshDecoded = 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); + + assert.equal(originalMatch, refreshMatch); + done(); + }); + it('Decoded version of a refreshed token should be the same, except for timing data', function (done) { var comparison = equal(originalDecoded, refreshDecoded); - console.log('comparison : ' + comparison); assert.ok(comparison); - assert.ok(refreshDecoded); + done(); + }); + + it('Refreshed token should have a later expiery time then the original', function (done) { + var originalExpiery = originalDecoded.payload.exp; + var refreshedExpiery = refreshDecoded.payload.exp; - //test that the refreshed token has a time in the future. + assert.isTrue((refreshedExpiery > originalExpiery), 'Refreshed expiery time is above original time'); done(); }); }); From 0428b23ea891382803b3c777e84052cdfeb00f55 Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Wed, 3 Feb 2016 10:16:20 -0500 Subject: [PATCH 05/11] typos in test and documentation in index.js --- index.js | 11 ++++++++++- test/refresh.tests.js | 6 +++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 9765e11..d29c039 100644 --- a/index.js +++ b/index.js @@ -272,10 +272,19 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, 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' 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. + //TODO: asynchronus function. var header; var payload; diff --git a/test/refresh.tests.js b/test/refresh.tests.js index c23e00c..c629a8b 100644 --- a/test/refresh.tests.js +++ b/test/refresh.tests.js @@ -118,10 +118,10 @@ describe('Refresh Token Testing', function() { }); it('Refreshed token should have a later expiery time then the original', function (done) { - var originalExpiery = originalDecoded.payload.exp; - var refreshedExpiery = refreshDecoded.payload.exp; + var originalExpiry = originalDecoded.payload.exp; + var refreshedExpiry = refreshDecoded.payload.exp; - assert.isTrue((refreshedExpiery > originalExpiery), 'Refreshed expiery time is above original time'); + assert.isTrue((refreshedExpiry > originalExpiry), 'Refreshed expiry time is above original time'); done(); }); }); From 9bd096144734ae1dc9c082535945caa0fd66cd90 Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Wed, 3 Feb 2016 10:49:41 -0500 Subject: [PATCH 06/11] Testing failures and async mode --- index.js | 24 ++++++++++++++++++--- test/refresh.tests.js | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index d29c039..0211459 100644 --- a/index.js +++ b/index.js @@ -284,7 +284,26 @@ JWT.verify = function(jwtString, secretOrPublicKey, options, callback) { */ 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. - //TODO: asynchronus function. + + 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; @@ -342,6 +361,5 @@ JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { options['expiresIn'] = expiresIn; newToken = JWT.sign(obj, secretOrPrivateKey, options); - return newToken; - //callback(null, newToken); + return done(null, newToken); }; diff --git a/test/refresh.tests.js b/test/refresh.tests.js index c629a8b..94b9bec 100644 --- a/test/refresh.tests.js +++ b/test/refresh.tests.js @@ -98,30 +98,80 @@ describe('Refresh Token Testing', function() { 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(); }); }); From 3286e3ca84872ad2d71a2de49236734162f05fe4 Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Wed, 3 Feb 2016 10:58:00 -0500 Subject: [PATCH 07/11] Added description for refresh in README.md --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 9d81555..6a2228d 100644 --- a/README.md +++ b/README.md @@ -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. From 6d3250d1b9e3a77003cff8c1da6a702f2c919cd5 Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Tue, 2 Feb 2016 09:21:26 -0500 Subject: [PATCH 08/11] According to issue #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 --- README.md | 22 ++++++ index.js | 93 ++++++++++++++++++++++ test/refresh.tests.js | 177 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 292 insertions(+) create mode 100644 test/refresh.tests.js diff --git a/README.md b/README.md index 9d81555..6a2228d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/index.js b/index.js index 221dba2..0211459 100644 --- a/index.js +++ b/index.js @@ -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); +}; diff --git a/test/refresh.tests.js b/test/refresh.tests.js new file mode 100644 index 0000000..94b9bec --- /dev/null +++ b/test/refresh.tests.js @@ -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(); + }); +}); From e122e2e0804921a5183ea1de9d37ff6985eadb4b Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Wed, 3 Feb 2016 13:02:15 -0500 Subject: [PATCH 09/11] Fix of => (arrow function) for function definition - not supported --- test/refresh.tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/refresh.tests.js b/test/refresh.tests.js index 94b9bec..7b9b422 100644 --- a/test/refresh.tests.js +++ b/test/refresh.tests.js @@ -19,7 +19,7 @@ var assert = require('chai').assert; * the symmetric test. * @return boolean - true if the tokens match. */ -var equal = (first, second, last) => { +var equal = function (first, second, last) { var noCompare = ['iat', 'exp']; var areEqual = true; From e4bc232726a510de23134406dca08787fb8779b3 Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Wed, 22 Jun 2016 10:47:19 -0400 Subject: [PATCH 10/11] Fixing for changes in naming from version 6.0.0 --- index.js | 4 ++-- test/refresh.tests.js | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 6f46f39..82fa24a 100644 --- a/index.js +++ b/index.js @@ -261,11 +261,11 @@ JWT.refresh = function(token, expiresIn, secretOrPrivateKey, callback) { } if(header) { - options.headers = { }; + 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.headers[key] = header[key]; + options.header[key] = header[key]; } else { options[optionMapping[key]] = header[key]; diff --git a/test/refresh.tests.js b/test/refresh.tests.js index 7b9b422..bde1bd1 100644 --- a/test/refresh.tests.js +++ b/test/refresh.tests.js @@ -3,7 +3,6 @@ var jws = require('jws'); var fs = require('fs'); var path = require('path'); var sinon = require('sinon'); - var assert = require('chai').assert; /** @@ -57,7 +56,7 @@ describe('Refresh Token Testing', function() { expiresIn: '3600', subject: 'Testing Refresh', issuer: 'node-jsonwebtoken', - headers: { + header: { a: 'header' } }; @@ -167,8 +166,8 @@ describe('Refresh Token Testing', function() { 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.equal(err.name, 'ValidationError'); + assert.equal(err.message, 'child "expiresIn" fails because ["expiresIn" must be a number, "expiresIn" must be a string]'); } assert.notOk(failRefresh); From 5f11be4d37b1c644cb01fbb48b67af33d7899c84 Mon Sep 17 00:00:00 2001 From: Jean-Philipe Pellerin Date: Fri, 29 Jul 2016 17:42:36 -0400 Subject: [PATCH 11/11] Refresh refactored and now verifies that the token is valid - more tests needed --- index.js | 94 +--------------------------------- refresh.js | 114 ++++++++++++++++++++++++++++++++++++++++++ test/refresh.tests.js | 52 ++++++------------- 3 files changed, 129 insertions(+), 131 deletions(-) create mode 100644 refresh.js diff --git a/index.js b/index.js index b9b8460..c710082 100644 --- a/index.js +++ b/index.js @@ -2,100 +2,8 @@ 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'), }; - -/** -* 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.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 = JWT.sign(obj, secretOrPrivateKey, options); - return done(null, newToken); -}; diff --git a/refresh.js b/refresh.js new file mode 100644 index 0000000..77a33c5 --- /dev/null +++ b/refresh.js @@ -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.'); + } +}; diff --git a/test/refresh.tests.js b/test/refresh.tests.js index bde1bd1..aa1b31a 100644 --- a/test/refresh.tests.js +++ b/test/refresh.tests.js @@ -92,32 +92,37 @@ describe('Refresh Token Testing', function() { }); it('Should be able to refresh the token', function (done) { - var refreshed = jwt.refresh(jwt.decode(token, {complete: true}), 3600, secret); + + var refreshed = jwt.refresh(token, 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) { + + jwt.refresh(token, 3600, secret, null, function(err, refreshedToken) { + assert.ok(refreshedToken); + done(); }); - done(); }); var originalDecoded = jwt.decode(token, {complete: true}); - var refreshed = jwt.refresh(originalDecoded, 3600, secret); + var refreshed = jwt.refresh(token, 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) { + jwt.refresh(token, 3600, secret, null, function(err, refreshedToken) { + refreshAsync = refreshedToken; - refreshAsyncDecoded = jwt.decode(refreshed, {complete: true}); + 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(originalDecoded, refreshAsyncDecoded); + var asyncRefreshMatch = equal(refreshAsyncDecoded, refreshAsyncDecoded); assert.equal(originalMatch, refreshMatch); assert.equal(originalMatch, asyncRefreshMatch); @@ -125,6 +130,7 @@ describe('Refresh Token Testing', function() { }); 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); @@ -134,6 +140,7 @@ describe('Refresh Token Testing', function() { }); 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; @@ -142,35 +149,4 @@ describe('Refresh Token Testing', function() { 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, 'ValidationError'); - assert.equal(err.message, 'child "expiresIn" fails because ["expiresIn" must be a number, "expiresIn" must be a string]'); - } - - assert.notOk(failRefresh); - done(); - }); });