-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from all commits
bebaf16
10f2f9f
8c81d34
5d8d288
0428b23
9bd0961
3286e3c
6d3250d
c1e4217
e122e2e
27c0eb0
ed9d449
e4bc232
063779b
5f11be4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it should be There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
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.'); | ||
} | ||
}; |
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(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good