diff --git a/lib/index.js b/lib/index.js index fb3625e..1e95d79 100755 --- a/lib/index.js +++ b/lib/index.js @@ -20,6 +20,9 @@ exports.register.attributes = { pkg: require('../package.json') }; +function isFunction(functionToCheck) { + return Object.prototype.toString.call(functionToCheck) === '[object Function]'; +} internals.implementation = function (server, options) { @@ -53,42 +56,50 @@ internals.implementation = function (server, options) { var token = parts[1]; - jwt.verify(token, settings.key, function(err, decoded) { - if(err && err.message === 'jwt expired') { - return reply(Boom.unauthorized('Expired token received for JSON Web Token validation', 'Bearer')); - } else if (err) { - return reply(Boom.unauthorized('Invalid signature received for JSON Web Token validation', 'Bearer')); - } + var getKey = isFunction(settings.key) ? + settings.key : + function(token, callback) { callback(null, settings.key); }; - if (!settings.validateFunc) { - return reply(null, { credentials: decoded }); - } + getKey(token, function(err, key){ + if (err) { return reply(Boom.wrap(err)); } + // handle err + jwt.verify(token, key, function(err, decoded) { + if(err && err.message === 'jwt expired') { + return reply(Boom.unauthorized('Expired token received for JSON Web Token validation', 'Bearer')); + } else if (err) { + return reply(Boom.unauthorized('Invalid signature received for JSON Web Token validation', 'Bearer')); + } - settings.validateFunc(decoded, function (err, isValid, credentials) { + if (!settings.validateFunc) { + return reply(null, { credentials: decoded }); + } - credentials = credentials || null; - if (err) { - return reply(err, { credentials: credentials, log: { tags: ['auth', 'jwt'], data: err } }); - } + settings.validateFunc(decoded, function (err, isValid, credentials) { - if (!isValid) { - return reply(Boom.unauthorized('Invalid token', 'Bearer'), { credentials: credentials }); - } + credentials = credentials || null; - if (!credentials || typeof credentials !== 'object') { + if (err) { + return reply(err, { credentials: credentials, log: { tags: ['auth', 'jwt'], data: err } }); + } - return reply(Boom.badImplementation('Bad credentials object received for jwt auth validation'), { log: { tags: 'credentials' } }); - } + if (!isValid) { + return reply(Boom.unauthorized('Invalid token', 'Bearer'), { credentials: credentials }); + } - // Authenticated + if (!credentials || typeof credentials !== 'object') { - return reply(null, { credentials: credentials }); - }); + return reply(Boom.badImplementation('Bad credentials object received for jwt auth validation'), { log: { tags: 'credentials' } }); + } - }); + // Authenticated + + return reply(null, { credentials: credentials }); + }); + }); + }); } }; diff --git a/test/dynamicKeys.tests.js b/test/dynamicKeys.tests.js new file mode 100644 index 0000000..5ff37f1 --- /dev/null +++ b/test/dynamicKeys.tests.js @@ -0,0 +1,116 @@ +// Load modules + +var Lab = require('lab'); +var Hapi = require('hapi'); +var Hoek = require('hoek') +var Boom = require('boom'); +var jwt = require('jsonwebtoken'); + + +// Test shortcuts + +var expect = Lab.expect; +var before = Lab.before; +var describe = Lab.experiment; +var it = Lab.test; + +describe('Dynamic Secret', function () { + var keys = { + 'john': 'johnkey', + 'jane': 'janekey' + }; + + var tokenHeader = function (username, options) { + if (!keys[username]){ + throw new Error('Invalid user name ' + username + '. Valid options \'john\' or \'jane\''); + } + + options = options || {}; + + return 'Bearer ' + jwt.sign({username: username}, keys[username], options); + }; + + var tokenHandler = function (request, reply) { + reply(request.auth.credentials.username); + }; + + var getKey = function(token, callback){ + getKey.lastToken = token; + var data = jwt.decode(token); + Hoek.nextTick(function(){ + callback(null, keys[data.username]); + })(); + } + + var errorGetKey = function(token, callback){ + callback(new Error('Failed')); + } + + var boomErrorGetKey = function(token, callback){ + callback(Boom.forbidden('forbidden')); + } + + var server = new Hapi.Server({ debug: false }); + + before(function (done) { + server.pack.register(require('../'), function (err) { + expect(err).to.not.exist; + server.auth.strategy('normalError', 'jwt', false, { key: errorGetKey }); + server.auth.strategy('boomError', 'jwt', false, { key: boomErrorGetKey }); + server.auth.strategy('default', 'jwt', false, { key: getKey }); + server.route([ + { method: 'POST', path: '/token', handler: tokenHandler, config: { auth: 'default' } }, + { method: 'POST', path: '/normalError', handler: tokenHandler, config: { auth: 'normalError' } }, + { method: 'POST', path: '/boomError', handler: tokenHandler, config: { auth: 'boomError' } } + ]); + + done(); + }); + }); + + ['jane', 'john'].forEach(function(user){ + + it('uses key function passing ' + user + '\'s token if ' + user + ' is user', function (done) { + + var request = { method: 'POST', url: '/token', headers: { authorization: tokenHeader(user) } }; + + server.inject(request, function (res) { + expect(res.result).to.exist; + expect(res.result).to.equal(user); + + jwt.verify(getKey.lastToken, keys[user], function(err, decoded){ + if (err) { return done(err); } + expect(decoded.username).to.equal(user); + + done(); + }); + }); + }); + }); + + it('return 500 if an is error thrown when getting key', function(done){ + + var request = { method: 'POST', url: '/normalError', headers: { authorization: tokenHeader('john') } }; + + server.inject(request, function (res) { + expect(res).to.exist; + expect(res.result.statusCode).to.equal(500); + expect(res.result.error).to.equal('Internal Server Error'); + expect(res.result.message).to.equal('An internal server error occurred'); + done(); + }); + }); + + it('return 403 if an is error thrown when getting key', function(done){ + + var request = { method: 'POST', url: '/boomError', headers: { authorization: tokenHeader('john') } }; + + server.inject(request, function (res) { + expect(res).to.exist; + expect(res.result.statusCode).to.equal(403); + expect(res.result.error).to.equal('Forbidden'); + expect(res.result.message).to.equal('forbidden'); + done(); + }); + }); +}); \ No newline at end of file