diff --git a/README.md b/README.md index d597665..139fa3e 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,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 161eb2d..c710082 100644 --- a/index.js +++ b/index.js @@ -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'), 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 new file mode 100644 index 0000000..aa1b31a --- /dev/null +++ b/test/refresh.tests.js @@ -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(); + }); +});