mirror of
https://github.com/hanzoai/node-jsonwebtoken.git
synced 2026-09-24 17:00:48 +00:00
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:
committed by
Eduardo Diaz
parent
ca3cd60408
commit
4d2a22c975
@@ -0,0 +1,3 @@
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user