Skip to content

Commit

Permalink
Secret callback revisited (#480)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
JacoKoster authored and ziluvatar committed Jun 11, 2018
1 parent 73c4a5a commit d01cc7b
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 104 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
@@ -0,0 +1,3 @@
[*]
indent_style = space
indent_size = 2
8 changes: 8 additions & 0 deletions .eslintrc
@@ -0,0 +1,8 @@
{
"env": {
"es6": true
},
"rules": {
"indent": [2,2]
}
}
18 changes: 18 additions & 0 deletions README.md
Expand Up @@ -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.

Expand Down Expand Up @@ -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])
Expand Down
3 changes: 1 addition & 2 deletions package.json
Expand Up @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions sign.js
@@ -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');
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
8 changes: 4 additions & 4 deletions test/keyid.tests.js
Expand Up @@ -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);
})
});
92 changes: 89 additions & 3 deletions test/verify.tests.js
Expand Up @@ -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'));
Expand All @@ -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) {
Expand Down Expand Up @@ -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';
Expand Down

0 comments on commit d01cc7b

Please sign in to comment.