From 0f9bd1e7549c28281653c402a1cef6d9c32e7114 Mon Sep 17 00:00:00 2001 From: dschenkelman Date: Tue, 7 Oct 2014 15:36:32 -0300 Subject: [PATCH 01/10] Added support for dynamic keys based on token info --- lib/index.js | 59 +++++++++++-------- test/dynamicKeys.tests.js | 116 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 test/dynamicKeys.tests.js diff --git a/lib/index.js b/lib/index.js index 396bdaa..67dd09c 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.continue({ 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.continue(null, { credentials: decoded }); + } - credentials = credentials || null; - if (err) { - return reply(err, null, { credentials: credentials }); - } + settings.validateFunc(decoded, function (err, isValid, credentials) { - if (!isValid) { - return reply(Boom.unauthorized('Invalid token', 'Bearer'), null, { credentials: credentials }); - } + credentials = credentials || null; - if (!credentials || typeof credentials !== 'object') { + if (err) { + return reply(err, null, { credentials: credentials, log: { tags: ['auth', 'jwt'], data: err } }); + } - return reply(Boom.badImplementation('Bad credentials object received for jwt auth validation'), null, { log: { tags: 'credentials' } }); - } + if (!isValid) { + return reply(Boom.unauthorized('Invalid token', 'Bearer'), null, { credentials: credentials }); + } - // Authenticated + if (!credentials || typeof credentials !== 'object') { - return reply.continue({ credentials: credentials }); - }); + return reply(Boom.badImplementation('Bad credentials object received for jwt auth validation'), null, { log: { tags: 'credentials' } }); + } - }); + // Authenticated + + return reply.continue(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 From 785a0ce4db21eb8e3e36863cc8e7614199d42d46 Mon Sep 17 00:00:00 2001 From: dschenkelman Date: Fri, 10 Oct 2014 17:18:47 -0300 Subject: [PATCH 02/10] Added extra info parameter retrieved when getting key and passed to validate --- lib/index.js | 5 ++--- test/dynamicKeys.tests.js | 27 +++++++++++++++++++++++++-- test/index.js | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/index.js b/lib/index.js index 67dd09c..0aead5b 100755 --- a/lib/index.js +++ b/lib/index.js @@ -60,7 +60,7 @@ internals.implementation = function (server, options) { settings.key : function(token, callback) { callback(null, settings.key); }; - getKey(token, function(err, key){ + getKey(token, function(err, key, extraInfo){ if (err) { return reply(Boom.wrap(err)); } // handle err @@ -75,8 +75,7 @@ internals.implementation = function (server, options) { return reply.continue(null, { credentials: decoded }); } - - settings.validateFunc(decoded, function (err, isValid, credentials) { + settings.validateFunc(decoded, extraInfo, function (err, isValid, credentials) { credentials = credentials || null; diff --git a/test/dynamicKeys.tests.js b/test/dynamicKeys.tests.js index 5ff37f1..c71fa0c 100644 --- a/test/dynamicKeys.tests.js +++ b/test/dynamicKeys.tests.js @@ -20,6 +20,11 @@ describe('Dynamic Secret', function () { 'jane': 'janekey' }; + var info = { + 'john': 'johninfo', + 'jane': 'janeinfo', + } + var tokenHeader = function (username, options) { if (!keys[username]){ throw new Error('Invalid user name ' + username + '. Valid options \'john\' or \'jane\''); @@ -38,10 +43,15 @@ describe('Dynamic Secret', function () { getKey.lastToken = token; var data = jwt.decode(token); Hoek.nextTick(function(){ - callback(null, keys[data.username]); + callback(null, keys[data.username], info[data.username]); })(); } + var validateFunc = function(decoded, extraInfo, callback){ + validateFunc.lastExtraInfo = extraInfo; + callback(null, true, decoded); + } + var errorGetKey = function(token, callback){ callback(new Error('Failed')); } @@ -57,7 +67,7 @@ describe('Dynamic Secret', function () { 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.auth.strategy('default', 'jwt', false, { key: getKey, validateFunc: validateFunc }); server.route([ { method: 'POST', path: '/token', handler: tokenHandler, config: { auth: 'default' } }, { method: 'POST', path: '/normalError', handler: tokenHandler, config: { auth: 'normalError' } }, @@ -86,6 +96,19 @@ describe('Dynamic Secret', function () { }); }); }); + + it('uses validateFunc function passing ' + user + '\'s extra info 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); + + expect(validateFunc.lastExtraInfo).to.equal(info[user]); + done(); + }); + }); }); it('return 500 if an is error thrown when getting key', function(done){ diff --git a/test/index.js b/test/index.js index 4d2e5a4..0810047 100755 --- a/test/index.js +++ b/test/index.js @@ -24,7 +24,7 @@ describe('Token', function () { return 'Bearer ' + Jwt.sign({username : username}, privateKey, options); }; - var loadUser = function (decodedToken, callback) { + var loadUser = function (decodedToken, _, callback) { var username = decodedToken.username; if (username === 'john') { From cc7fe53bd4797c630f21835bd5c88716518e15f6 Mon Sep 17 00:00:00 2001 From: dschenkelman Date: Tue, 16 Dec 2014 16:41:38 -0300 Subject: [PATCH 03/10] Migrated to hapi 8 --- lib/index.js | 4 ++-- test/dynamicKeys.tests.js | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/index.js b/lib/index.js index 0aead5b..5f56b76 100755 --- a/lib/index.js +++ b/lib/index.js @@ -72,7 +72,7 @@ internals.implementation = function (server, options) { } if (!settings.validateFunc) { - return reply.continue(null, { credentials: decoded }); + return reply.continue({ credentials: decoded }); } settings.validateFunc(decoded, extraInfo, function (err, isValid, credentials) { @@ -94,7 +94,7 @@ internals.implementation = function (server, options) { // Authenticated - return reply.continue(null, { credentials: credentials }); + return reply.continue({ credentials: credentials }); }); }); diff --git a/test/dynamicKeys.tests.js b/test/dynamicKeys.tests.js index c71fa0c..260290e 100644 --- a/test/dynamicKeys.tests.js +++ b/test/dynamicKeys.tests.js @@ -2,6 +2,7 @@ var Lab = require('lab'); var Hapi = require('hapi'); +var Code = require('code'); var Hoek = require('hoek') var Boom = require('boom'); var jwt = require('jsonwebtoken'); @@ -9,10 +10,11 @@ var jwt = require('jsonwebtoken'); // Test shortcuts -var expect = Lab.expect; -var before = Lab.before; -var describe = Lab.experiment; -var it = Lab.test; +var lab = exports.lab = Lab.script(); +var expect = Code.expect; +var before = lab.before; +var describe = lab.describe; +var it = lab.it; describe('Dynamic Secret', function () { var keys = { @@ -61,9 +63,10 @@ describe('Dynamic Secret', function () { } var server = new Hapi.Server({ debug: false }); + server.connection(); before(function (done) { - server.pack.register(require('../'), function (err) { + server.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 }); From f60adb5fb45b94c85e4b0cae45c9b243bc2befc5 Mon Sep 17 00:00:00 2001 From: dschenkelman Date: Mon, 22 Dec 2014 17:29:23 -0300 Subject: [PATCH 04/10] 2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d0b61d..e074583 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hapi-auth-jwt", "description": "JSON Web Token (JWT) authentication plugin", - "version": "2.0.0", + "version": "2.1.0", "author": "Ryan Fitzgerald ", "repository": "git://github.com/ryanfitz/hapi-auth-jwt", "main": "index", From f549c460c399807ded2de023b005fafc645ef3a2 Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Wed, 11 Feb 2015 16:41:33 -0800 Subject: [PATCH 05/10] updated README to reflect dynamic keys --- README.md | 103 ++++++++++++++++++++++++++++++++++---- test/dynamicKeys.tests.js | 14 +++--- 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index eb7ba62..8c75ce6 100755 --- a/README.md +++ b/README.md @@ -6,15 +6,19 @@ JSON Web Token authentication requires verifying a signed token. The `'jwt'` scheme takes the following options: -- `key` - (required) The private key the token was signed with. -- `validateFunc` - (optional) validation and user lookup function with the signature `function(token, callback)` where: +- `key` - (required) Either the private key the token was signed with or a key lookup function with signature `function(token, callback)` where: + - `token` - the *unverified* encoded jwt token + - `callback` - a callback function with the signature `function(err, key, extraInfo)` where: + - `err` - an internal error + - `key` - the private key that will be used to verify the token + - `extraInfo` - data that will be passed to `validateFunc` (e.g. credentials) +- `validateFunc` - (optional) validation and user lookup function with the signature `function(token, extraInfo, callback)` where: - `token` - the verified and decoded jwt token + - `extraInfo` - data that was passed from the key lookup function (e.g. credentials) - `callback` - a callback function with the signature `function(err, isValid, credentials)` where: - `err` - an internal error. - `isValid` - `true` if the token was valid otherwise `false`. - - `credentials` - a credentials object passed back to the application in `request.auth.credentials`. Typically, `credentials` are only - included when `isValid` is `true`, but there are cases when the application needs to know who tried to authenticate even when it fails - (e.g. with authentication mode `'try'`). + - `credentials` - a credentials object passed back to the application in `request.auth.credentials`. Typically, `credentials` are only included when `isValid` is `true`, but there are cases when the application needs to know who tried to authenticate even when it fails (e.g. with authentication mode `'try'`). See the example folder for an executable example. @@ -26,7 +30,6 @@ var Hapi = require('hapi'), server.connection({ port: 8080 }); - var accounts = { 123: { id: 123, @@ -36,7 +39,6 @@ var accounts = { } }; - var privateKey = 'BbZJjyoXAdr8BUZuiKKARWimKfrSmQ6fv8kZ7OFfc'; // Use this token to build your request with the 'Authorization' header. @@ -44,8 +46,7 @@ var privateKey = 'BbZJjyoXAdr8BUZuiKKARWimKfrSmQ6fv8kZ7OFfc'; // Authorization: Bearer var token = jwt.sign({ accountId: 123 }, privateKey); - -var validate = function (decodedToken, callback) { +var validate = function (decodedToken, extraInfo, callback) { var error, credentials = accounts[decodedToken.accountId] || {}; @@ -57,7 +58,6 @@ var validate = function (decodedToken, callback) { return callback(error, true, credentials) }; - server.register(require('hapi-auth-jwt'), function (error) { server.auth.strategy('token', 'jwt', { @@ -86,6 +86,89 @@ server.register(require('hapi-auth-jwt'), function (error) { }); }); +server.start(); + +``` + +With a key lookup method: + +```javascript + +var Hapi = require('hapi'), + jwt = require('jsonwebtoken'), + server = new Hapi.Server(); + +server.connection({ port: 8080 }); + +var accounts = { + 123: { + id: 123, + user: 'john', + fullName: 'John Doe', + privateKey: 'BbZJjyoXAdr8BUZuiKKARWimKfrSmQ6fv8kZ7OFfc' + scope: ['a', 'b'] + } +}; + +var getKey = function(token, callback) { + var data = jwt.decode(token); + var account = accounts[data.id]; + var key = account.privateKey; + + var credentials = { + id: account.id, + user: account.user + fullName: account.fullName, + scope: account.scope + } + + callback(null, key, credentials); +} + +// Use this token to build your request with the 'Authorization' header. +// Ex: +// Authorization: Bearer +var token = jwt.sign({ accountId: 123 }, privateKey); + +var validate = function (decodedToken, extraInfo, callback) { + + var error, + credentials = extraInfo || {}; + + if (!credentials) { + return callback(error, false, credentials); + } + + return callback(error, true, credentials) +}; + +server.register(require('hapi-auth-jwt'), function (error) { + + server.auth.strategy('token', 'jwt', { + key: getKey, + validateFunc: validate + }); + + server.route({ + method: 'GET', + path: '/', + config: { + auth: 'token' + } + }); + + // With scope requirements + server.route({ + method: 'GET', + path: '/withScope', + config: { + auth: { + strategy: 'token', + scope: ['a'] + } + } + }); +}); server.start(); diff --git a/test/dynamicKeys.tests.js b/test/dynamicKeys.tests.js index 260290e..6691646 100644 --- a/test/dynamicKeys.tests.js +++ b/test/dynamicKeys.tests.js @@ -3,7 +3,7 @@ var Lab = require('lab'); var Hapi = require('hapi'); var Code = require('code'); -var Hoek = require('hoek') +var Hoek = require('hoek'); var Boom = require('boom'); var jwt = require('jsonwebtoken'); @@ -25,7 +25,7 @@ describe('Dynamic Secret', function () { var info = { 'john': 'johninfo', 'jane': 'janeinfo', - } + }; var tokenHeader = function (username, options) { if (!keys[username]){ @@ -47,20 +47,20 @@ describe('Dynamic Secret', function () { Hoek.nextTick(function(){ callback(null, keys[data.username], info[data.username]); })(); - } + }; var validateFunc = function(decoded, extraInfo, callback){ validateFunc.lastExtraInfo = extraInfo; callback(null, true, decoded); - } + }; 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 }); server.connection(); @@ -139,4 +139,4 @@ describe('Dynamic Secret', function () { done(); }); }); -}); \ No newline at end of file +}); From eb499948f5ceb62bd5521084690a7706b7dcc46a Mon Sep 17 00:00:00 2001 From: dschenkelman Date: Tue, 7 Oct 2014 15:36:32 -0300 Subject: [PATCH 06/10] Added support for dynamic keys based on token info --- lib/index.js | 59 +++++++++++-------- test/dynamicKeys.tests.js | 116 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 test/dynamicKeys.tests.js diff --git a/lib/index.js b/lib/index.js index 396bdaa..67dd09c 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.continue({ 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.continue(null, { credentials: decoded }); + } - credentials = credentials || null; - if (err) { - return reply(err, null, { credentials: credentials }); - } + settings.validateFunc(decoded, function (err, isValid, credentials) { - if (!isValid) { - return reply(Boom.unauthorized('Invalid token', 'Bearer'), null, { credentials: credentials }); - } + credentials = credentials || null; - if (!credentials || typeof credentials !== 'object') { + if (err) { + return reply(err, null, { credentials: credentials, log: { tags: ['auth', 'jwt'], data: err } }); + } - return reply(Boom.badImplementation('Bad credentials object received for jwt auth validation'), null, { log: { tags: 'credentials' } }); - } + if (!isValid) { + return reply(Boom.unauthorized('Invalid token', 'Bearer'), null, { credentials: credentials }); + } - // Authenticated + if (!credentials || typeof credentials !== 'object') { - return reply.continue({ credentials: credentials }); - }); + return reply(Boom.badImplementation('Bad credentials object received for jwt auth validation'), null, { log: { tags: 'credentials' } }); + } - }); + // Authenticated + + return reply.continue(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 From 853958598696ad80d330518464a59f7ba32f0e3e Mon Sep 17 00:00:00 2001 From: dschenkelman Date: Fri, 10 Oct 2014 17:18:47 -0300 Subject: [PATCH 07/10] Added extra info parameter retrieved when getting key and passed to validate --- lib/index.js | 5 ++--- test/dynamicKeys.tests.js | 27 +++++++++++++++++++++++++-- test/index.js | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/index.js b/lib/index.js index 67dd09c..0aead5b 100755 --- a/lib/index.js +++ b/lib/index.js @@ -60,7 +60,7 @@ internals.implementation = function (server, options) { settings.key : function(token, callback) { callback(null, settings.key); }; - getKey(token, function(err, key){ + getKey(token, function(err, key, extraInfo){ if (err) { return reply(Boom.wrap(err)); } // handle err @@ -75,8 +75,7 @@ internals.implementation = function (server, options) { return reply.continue(null, { credentials: decoded }); } - - settings.validateFunc(decoded, function (err, isValid, credentials) { + settings.validateFunc(decoded, extraInfo, function (err, isValid, credentials) { credentials = credentials || null; diff --git a/test/dynamicKeys.tests.js b/test/dynamicKeys.tests.js index 5ff37f1..c71fa0c 100644 --- a/test/dynamicKeys.tests.js +++ b/test/dynamicKeys.tests.js @@ -20,6 +20,11 @@ describe('Dynamic Secret', function () { 'jane': 'janekey' }; + var info = { + 'john': 'johninfo', + 'jane': 'janeinfo', + } + var tokenHeader = function (username, options) { if (!keys[username]){ throw new Error('Invalid user name ' + username + '. Valid options \'john\' or \'jane\''); @@ -38,10 +43,15 @@ describe('Dynamic Secret', function () { getKey.lastToken = token; var data = jwt.decode(token); Hoek.nextTick(function(){ - callback(null, keys[data.username]); + callback(null, keys[data.username], info[data.username]); })(); } + var validateFunc = function(decoded, extraInfo, callback){ + validateFunc.lastExtraInfo = extraInfo; + callback(null, true, decoded); + } + var errorGetKey = function(token, callback){ callback(new Error('Failed')); } @@ -57,7 +67,7 @@ describe('Dynamic Secret', function () { 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.auth.strategy('default', 'jwt', false, { key: getKey, validateFunc: validateFunc }); server.route([ { method: 'POST', path: '/token', handler: tokenHandler, config: { auth: 'default' } }, { method: 'POST', path: '/normalError', handler: tokenHandler, config: { auth: 'normalError' } }, @@ -86,6 +96,19 @@ describe('Dynamic Secret', function () { }); }); }); + + it('uses validateFunc function passing ' + user + '\'s extra info 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); + + expect(validateFunc.lastExtraInfo).to.equal(info[user]); + done(); + }); + }); }); it('return 500 if an is error thrown when getting key', function(done){ diff --git a/test/index.js b/test/index.js index 4d2e5a4..0810047 100755 --- a/test/index.js +++ b/test/index.js @@ -24,7 +24,7 @@ describe('Token', function () { return 'Bearer ' + Jwt.sign({username : username}, privateKey, options); }; - var loadUser = function (decodedToken, callback) { + var loadUser = function (decodedToken, _, callback) { var username = decodedToken.username; if (username === 'john') { From 009138a0703387a69e9f785fb1a45d36efb1edee Mon Sep 17 00:00:00 2001 From: dschenkelman Date: Tue, 16 Dec 2014 16:41:38 -0300 Subject: [PATCH 08/10] Migrated to hapi 8 --- lib/index.js | 4 ++-- test/dynamicKeys.tests.js | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/index.js b/lib/index.js index 0aead5b..5f56b76 100755 --- a/lib/index.js +++ b/lib/index.js @@ -72,7 +72,7 @@ internals.implementation = function (server, options) { } if (!settings.validateFunc) { - return reply.continue(null, { credentials: decoded }); + return reply.continue({ credentials: decoded }); } settings.validateFunc(decoded, extraInfo, function (err, isValid, credentials) { @@ -94,7 +94,7 @@ internals.implementation = function (server, options) { // Authenticated - return reply.continue(null, { credentials: credentials }); + return reply.continue({ credentials: credentials }); }); }); diff --git a/test/dynamicKeys.tests.js b/test/dynamicKeys.tests.js index c71fa0c..260290e 100644 --- a/test/dynamicKeys.tests.js +++ b/test/dynamicKeys.tests.js @@ -2,6 +2,7 @@ var Lab = require('lab'); var Hapi = require('hapi'); +var Code = require('code'); var Hoek = require('hoek') var Boom = require('boom'); var jwt = require('jsonwebtoken'); @@ -9,10 +10,11 @@ var jwt = require('jsonwebtoken'); // Test shortcuts -var expect = Lab.expect; -var before = Lab.before; -var describe = Lab.experiment; -var it = Lab.test; +var lab = exports.lab = Lab.script(); +var expect = Code.expect; +var before = lab.before; +var describe = lab.describe; +var it = lab.it; describe('Dynamic Secret', function () { var keys = { @@ -61,9 +63,10 @@ describe('Dynamic Secret', function () { } var server = new Hapi.Server({ debug: false }); + server.connection(); before(function (done) { - server.pack.register(require('../'), function (err) { + server.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 }); From 932ede04bf06671227df53d5fe264a4c675f8545 Mon Sep 17 00:00:00 2001 From: dschenkelman Date: Mon, 16 Mar 2015 21:47:35 -0300 Subject: [PATCH 09/10] Using jsonwebtoken 4.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0db84a1..55193a4 100755 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "boom": "2.x.x", "hoek": "2.x.x", - "jsonwebtoken": "3.2.x" + "jsonwebtoken": "^4.2.0" }, "peerDependencies": { "hapi": ">=8.x.x" From 6b0fc2a0f11282d65d64da3ce1dd6916b19d53fe Mon Sep 17 00:00:00 2001 From: dschenkelman Date: Mon, 16 Mar 2015 21:48:08 -0300 Subject: [PATCH 10/10] 2.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55193a4..374fe50 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hapi-auth-jwt", "description": "JSON Web Token (JWT) authentication plugin", - "version": "2.1.0", + "version": "2.1.1", "author": "Ryan Fitzgerald ", "repository": "git://github.com/ryanfitz/hapi-auth-jwt", "main": "index",