Secret callback revisited (#480)

* Introduction of the secret callback

Without the more contentious 'none'-changes

* Removed some spaces...

I should really add a editor.config and eslint to this project ;-)

* Removed xtend as a dependency, as the native Object.Assign can do this as well

* Removed xtend as a dependency, as the native Object.Assign can do this as well

* Resolve feedback from review

* Added extra test and fixed the associated bug

* The return of the header

* Forgot to change this one as well... Sorry bout that

* Updated the readme and made the if-statements consistent

* Space; The final frontier
This commit is contained in:
Jaco Koster
2018-06-11 17:32:29 +02:00
committed by Eduardo Diaz
parent ca3cd60408
commit 4d2a22c975
8 changed files with 249 additions and 119 deletions
+3
View File
@@ -0,0 +1,3 @@
[*]
indent_style = space
indent_size = 2
+8
View File
@@ -0,0 +1,8 @@
{
"env": {
"es6": true
},
"rules": {
"indent": [2,2]
}
}
+18
View File
@@ -122,6 +122,7 @@ jwt.sign({
`secretOrPublicKey` is a string or buffer containing either the secret for HMAC algorithms, or the PEM
encoded public key for RSA and ECDSA.
If `jwt.verify` is called asynchronous, `secretOrPublicKey` can be a function that should fetch the secret or public key. See below for a detailed example
As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues/208#issuecomment-231861138), there are other libraries that expect base64 encoded secrets (random bytes encoded using base64), if that is your case you can pass `Buffer.from(secret, 'base64')`, by doing this the secret will be decoded using base64 and the token verification will use the original random bytes.
@@ -197,6 +198,23 @@ jwt.verify(token, cert, { algorithms: ['RS256'] }, function (err, payload) {
// if token alg != RS256, err == invalid signature
});
// Verify using getKey callback
// Example uses https://github.com/auth0/node-jwks-rsa as a way to fetch the keys.
var jwksClient = require('jwks-rsa');
var client = jwksClient({
jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json'
});
function getKey(header, callback){
client.getSigningKey(header.kid, function(err, key) {
var signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
jwt.verify(token, getKey, options, function(err, decoded) {
console.log(decoded.foo) // bar
});
```
### jwt.decode(token [, options])
+1 -2
View File
@@ -27,8 +27,7 @@
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"xtend": "^4.0.1"
"ms": "^2.1.1"
},
"devDependencies": {
"atob": "^1.1.2",
+2 -3
View File
@@ -1,5 +1,4 @@
var timespan = require('./lib/timespan');
var xtend = require('xtend');
var jws = require('jws');
var includes = require('lodash.includes');
var isBoolean = require('lodash.isboolean');
@@ -85,7 +84,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) {
var isObjectPayload = typeof payload === 'object' &&
!Buffer.isBuffer(payload);
var header = xtend({
var header = Object.assign({
alg: options.algorithm || 'HS256',
typ: isObjectPayload ? 'JWT' : undefined,
kid: options.keyid
@@ -112,7 +111,7 @@ module.exports = function (payload, secretOrPrivateKey, options, callback) {
return failure(error);
}
if (!options.mutatePayload) {
payload = xtend(payload);
payload = Object.assign({},payload);
}
} else {
var invalid_options = options_for_objects.filter(function (opt) {
+4 -4
View File
@@ -2,8 +2,8 @@ var jwt = require('../index');
var claims = {"name": "doron", "age": 46};
jwt.sign(claims, 'secret', {"keyid": "1234"}, function(err, good) {
console.log(jwt.decode(good, {"complete": true}).header.kid);
jwt.verify(good, 'secret', function(err, result) {
console.log(result);
})
console.log(jwt.decode(good, {"complete": true}).header.kid);
jwt.verify(good, 'secret', function(err, result) {
console.log(result);
})
});
+89 -3
View File
@@ -3,8 +3,10 @@ var jws = require('jws');
var fs = require('fs');
var path = require('path');
var sinon = require('sinon');
var JsonWebTokenError = require('../lib/JsonWebTokenError');
var assert = require('chai').assert;
var expect = require('chai').expect;
describe('verify', function() {
var pub = fs.readFileSync(path.join(__dirname, 'pub.pem'));
@@ -16,9 +18,9 @@ describe('verify', function() {
var signed = jws.sign({
header: header,
payload: payload,
secret: priv,
encoding: 'utf8'
payload: payload,
secret: priv,
encoding: 'utf8'
});
jwt.verify(signed, pub, {typ: 'JWT'}, function(err, p) {
@@ -67,6 +69,90 @@ describe('verify', function() {
});
});
describe('secret or token as callback', function () {
var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU';
var key = 'key';
var payload = { foo: 'bar', iat: 1437018582, exp: 1437018592 };
var options = {algorithms: ['HS256'], ignoreExpiration: true};
it('without callback', function (done) {
jwt.verify(token, key, options, function (err, p) {
assert.isNull(err);
assert.deepEqual(p, payload);
done();
});
});
it('simple callback', function (done) {
var keyFunc = function(header, callback) {
assert.deepEqual(header, { alg: 'HS256', typ: 'JWT' });
callback(undefined, key);
};
jwt.verify(token, keyFunc, options, function (err, p) {
assert.isNull(err);
assert.deepEqual(p, payload);
done();
});
});
it('should error if called synchronously', function (done) {
var keyFunc = function(header, callback) {
callback(undefined, key);
};
expect(function () {
jwt.verify(token, keyFunc, options);
}).to.throw(JsonWebTokenError, /verify must be called asynchronous if secret or public key is provided as a callback/);
done();
});
it('simple error', function (done) {
var keyFunc = function(header, callback) {
callback(new Error('key not found'));
};
jwt.verify(token, keyFunc, options, function (err, p) {
assert.equal(err.name, 'JsonWebTokenError');
assert.match(err.message, /error in secret or public key callback/);
assert.isUndefined(p);
done();
});
});
it('delayed callback', function (done) {
var keyFunc = function(header, callback) {
setTimeout(function() {
callback(undefined, key);
}, 25);
};
jwt.verify(token, keyFunc, options, function (err, p) {
assert.isNull(err);
assert.deepEqual(p, payload);
done();
});
});
it('delayed error', function (done) {
var keyFunc = function(header, callback) {
setTimeout(function() {
callback(new Error('key not found'));
}, 25);
};
jwt.verify(token, keyFunc, options, function (err, p) {
assert.equal(err.name, 'JsonWebTokenError');
assert.match(err.message, /error in secret or public key callback/);
assert.isUndefined(p);
done();
});
});
});
describe('expiration', function () {
// { foo: 'bar', iat: 1437018582, exp: 1437018592 }
var token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODU5Mn0.3aR3vocmgRpG05rsI9MpR6z2T_BGtMQaPq2YR6QaroU';
+124 -107
View File
@@ -4,7 +4,6 @@ var TokenExpiredError = require('./lib/TokenExpiredError');
var decode = require('./decode');
var timespan = require('./lib/timespan');
var jws = require('jws');
var xtend = require('xtend');
module.exports = function (jwtString, secretOrPublicKey, options, callback) {
if ((typeof options === 'function') && !callback) {
@@ -17,7 +16,8 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
}
//clone this object since we are going to mutate it.
options = xtend(options);
options = Object.assign({}, options);
var done;
if (callback) {
@@ -49,33 +49,10 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
return done(new JsonWebTokenError('jwt malformed'));
}
var hasSignature = parts[2].trim() !== '';
if (!hasSignature && secretOrPublicKey){
return done(new JsonWebTokenError('jwt signature is required'));
}
if (hasSignature && !secretOrPublicKey) {
return done(new JsonWebTokenError('secret or public key must be provided'));
}
if (!hasSignature && !options.algorithms) {
options.algorithms = ['none'];
}
if (!options.algorithms) {
options.algorithms = ~secretOrPublicKey.toString().indexOf('BEGIN CERTIFICATE') ||
~secretOrPublicKey.toString().indexOf('BEGIN PUBLIC KEY') ?
[ 'RS256','RS384','RS512','ES256','ES384','ES512' ] :
~secretOrPublicKey.toString().indexOf('BEGIN RSA PUBLIC KEY') ?
[ 'RS256','RS384','RS512' ] :
[ 'HS256','HS384','HS512' ];
}
var decodedToken;
try {
decodedToken = jws.decode(jwtString);
decodedToken = decode(jwtString, { complete: true });
} catch(err) {
return done(err);
}
@@ -85,97 +62,137 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
}
var header = decodedToken.header;
var getSecret;
if (!~options.algorithms.indexOf(header.alg)) {
return done(new JsonWebTokenError('invalid algorithm'));
}
var valid;
try {
valid = jws.verify(jwtString, header.alg, secretOrPublicKey);
} catch (e) {
return done(e);
}
if (!valid)
return done(new JsonWebTokenError('invalid signature'));
var payload;
try {
payload = decode(jwtString);
} catch(err) {
return done(err);
}
if (typeof payload.nbf !== 'undefined' && !options.ignoreNotBefore) {
if (typeof payload.nbf !== 'number') {
return done(new JsonWebTokenError('invalid nbf value'));
}
if (payload.nbf > clockTimestamp + (options.clockTolerance || 0)) {
return done(new NotBeforeError('jwt not active', new Date(payload.nbf * 1000)));
if(typeof secretOrPublicKey === 'function') {
if(!callback) {
return done(new JsonWebTokenError('verify must be called asynchronous if secret or public key is provided as a callback'));
}
getSecret = secretOrPublicKey;
}
else {
getSecret = function(header, secretCallback) {
return secretCallback(null, secretOrPublicKey);
};
}
if (typeof payload.exp !== 'undefined' && !options.ignoreExpiration) {
if (typeof payload.exp !== 'number') {
return done(new JsonWebTokenError('invalid exp value'));
return getSecret(header, function(err, secretOrPublicKey) {
if(err) {
return done(new JsonWebTokenError('error in secret or public key callback: ' + err.message));
}
if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) {
return done(new TokenExpiredError('jwt expired', new Date(payload.exp * 1000)));
var hasSignature = parts[2].trim() !== '';
if (!hasSignature && secretOrPublicKey){
return done(new JsonWebTokenError('jwt signature is required'));
}
}
if (options.audience) {
var audiences = Array.isArray(options.audience)? options.audience : [options.audience];
var target = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
if (hasSignature && !secretOrPublicKey) {
return done(new JsonWebTokenError('secret or public key must be provided'));
}
var match = target.some(function(targetAudience) {
return audiences.some(function(audience) {
return audience instanceof RegExp ? audience.test(targetAudience) : audience === targetAudience;
if (!hasSignature && !options.algorithms) {
options.algorithms = ['none'];
}
if (!options.algorithms) {
options.algorithms = ~secretOrPublicKey.toString().indexOf('BEGIN CERTIFICATE') ||
~secretOrPublicKey.toString().indexOf('BEGIN PUBLIC KEY') ?
['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'] :
~secretOrPublicKey.toString().indexOf('BEGIN RSA PUBLIC KEY') ?
['RS256', 'RS384', 'RS512'] :
['HS256', 'HS384', 'HS512'];
}
if (!~options.algorithms.indexOf(decodedToken.header.alg)) {
return done(new JsonWebTokenError('invalid algorithm'));
}
var valid;
try {
valid = jws.verify(jwtString, decodedToken.header.alg, secretOrPublicKey);
} catch (e) {
return done(e);
}
if (!valid) {
return done(new JsonWebTokenError('invalid signature'));
}
var payload = decodedToken.payload;
if (typeof payload.nbf !== 'undefined' && !options.ignoreNotBefore) {
if (typeof payload.nbf !== 'number') {
return done(new JsonWebTokenError('invalid nbf value'));
}
if (payload.nbf > clockTimestamp + (options.clockTolerance || 0)) {
return done(new NotBeforeError('jwt not active', new Date(payload.nbf * 1000)));
}
}
if (typeof payload.exp !== 'undefined' && !options.ignoreExpiration) {
if (typeof payload.exp !== 'number') {
return done(new JsonWebTokenError('invalid exp value'));
}
if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) {
return done(new TokenExpiredError('jwt expired', new Date(payload.exp * 1000)));
}
}
if (options.audience) {
var audiences = Array.isArray(options.audience) ? options.audience : [options.audience];
var target = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
var match = target.some(function (targetAudience) {
return audiences.some(function (audience) {
return audience instanceof RegExp ? audience.test(targetAudience) : audience === targetAudience;
});
});
});
if (!match)
return done(new JsonWebTokenError('jwt audience invalid. expected: ' + audiences.join(' or ')));
}
if (options.issuer) {
var invalid_issuer =
(typeof options.issuer === 'string' && payload.iss !== options.issuer) ||
(Array.isArray(options.issuer) && options.issuer.indexOf(payload.iss) === -1);
if (invalid_issuer) {
return done(new JsonWebTokenError('jwt issuer invalid. expected: ' + options.issuer));
}
}
if (options.subject) {
if (payload.sub !== options.subject) {
return done(new JsonWebTokenError('jwt subject invalid. expected: ' + options.subject));
}
}
if (options.jwtid) {
if (payload.jti !== options.jwtid) {
return done(new JsonWebTokenError('jwt jwtid invalid. expected: ' + options.jwtid));
}
}
if (options.maxAge) {
if (typeof payload.iat !== 'number') {
return done(new JsonWebTokenError('iat required when maxAge is specified'));
if (!match) {
return done(new JsonWebTokenError('jwt audience invalid. expected: ' + audiences.join(' or ')));
}
}
var maxAgeTimestamp = timespan(options.maxAge, payload.iat);
if (typeof maxAgeTimestamp === 'undefined') {
return done(new JsonWebTokenError('"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
}
if (clockTimestamp >= maxAgeTimestamp + (options.clockTolerance || 0)) {
return done(new TokenExpiredError('maxAge exceeded', new Date(maxAgeTimestamp * 1000)));
}
}
if (options.issuer) {
var invalid_issuer =
(typeof options.issuer === 'string' && payload.iss !== options.issuer) ||
(Array.isArray(options.issuer) && options.issuer.indexOf(payload.iss) === -1);
return done(null, payload);
if (invalid_issuer) {
return done(new JsonWebTokenError('jwt issuer invalid. expected: ' + options.issuer));
}
}
if (options.subject) {
if (payload.sub !== options.subject) {
return done(new JsonWebTokenError('jwt subject invalid. expected: ' + options.subject));
}
}
if (options.jwtid) {
if (payload.jti !== options.jwtid) {
return done(new JsonWebTokenError('jwt jwtid invalid. expected: ' + options.jwtid));
}
}
if (options.maxAge) {
if (typeof payload.iat !== 'number') {
return done(new JsonWebTokenError('iat required when maxAge is specified'));
}
var maxAgeTimestamp = timespan(options.maxAge, payload.iat);
if (typeof maxAgeTimestamp === 'undefined') {
return done(new JsonWebTokenError('"maxAge" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60'));
}
if (clockTimestamp >= maxAgeTimestamp + (options.clockTolerance || 0)) {
return done(new TokenExpiredError('maxAge exceeded', new Date(maxAgeTimestamp * 1000)));
}
}
return done(null, payload);
});
};