diff --git a/lib/index.js b/lib/index.js index ed93cacd..5dc082fd 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2,14 +2,14 @@ var jwt = require('jsonwebtoken'); var unless = require('express-unless'); var restify = require('restify'); var async = require('async'); +var set = require('lodash.set'); var InvalidCredentialsError = require('restify-errors').InvalidCredentialsError; var DEFAULT_REVOKED_FUNCTION = function(_, __, cb) { return cb(null, false); }; -var getClass = {}.toString; function isFunction(object) { - return object && getClass.call(object) == '[object Function]'; + return Object.prototype.toString.call(object) === '[object Function]'; } function wrapStaticSecretInCallback(secret){ @@ -30,6 +30,7 @@ module.exports = function(options) { var isRevokedCallback = options.isRevoked || DEFAULT_REVOKED_FUNCTION; var _requestProperty = options.userProperty || options.requestProperty || 'user'; + var _resultProperty = options.resultProperty; var credentialsRequired = typeof options.credentialsRequired === 'undefined' ? true : options.credentialsRequired; var middleware = function(req, res, next) { @@ -61,7 +62,11 @@ module.exports = function(options) { if (/^Bearer$/i.test(scheme)) { token = credentials; } else { - return next(new InvalidCredentialsError('Format is Authorization: Bearer [token]')); + if (credentialsRequired) { + return next(new InvalidCredentialsError('Format is Authorization: Bearer [token]')); + } else { + return next(); + } } } else { return next(new InvalidCredentialsError('Format is Authorization: Bearer [token]')); @@ -76,10 +81,16 @@ module.exports = function(options) { } } - var dtoken = jwt.decode(token, { complete: true }) || {}; + var dtoken; - async.parallel([ - function(callback){ + try { + dtoken = jwt.decode(token, { complete: true }) || {}; + } catch (err) { + return next(new InvalidCredentialsError('Invalid token')); + } + + async.waterfall([ + function getSecret(callback){ var arity = secretCallback.length; if (arity == 4) { secretCallback(req, dtoken.header, dtoken.payload, callback); @@ -87,24 +98,36 @@ module.exports = function(options) { secretCallback(req, dtoken.payload, callback); } }, - function(callback){ - isRevokedCallback(req, dtoken.payload, callback); + function verifyToken(secret, callback) { + jwt.verify(token, secret, options, function(err, decoded) { + if (err) { + callback(new InvalidCredentialsError('Invalid token')); + } else { + callback(null, decoded); + } + }); + }, + function checkRevoked(decoded, callback) { + isRevokedCallback(req, dtoken.payload, function (err, revoked) { + if (err) { + callback(err); + } + else if (revoked) { + callback(new restify.UnauthorizedError('The token has been revoked.')); + } else { + callback(null, decoded); + } + }); } - ], function(err, results){ + + ], function (err, result){ if (err) { return next(err); } - var revoked = results[1]; - if (revoked){ - return next(new restify.UnauthorizedError('The token has been revoked.')); + if (_resultProperty) { + set(res, _resultProperty, result); + } else { + set(req, _requestProperty, result); } - - var secret = results[0]; - - jwt.verify(token, secret, options, function(err, decoded) { - if (err && credentialsRequired) return next(new InvalidCredentialsError(err)); - - req[_requestProperty] = decoded; - next(); - }); + next(); }); }; diff --git a/package.json b/package.json index 32321649..de9a176e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "restify-jwt", - "version": "0.4.0", + "version": "0.5.0", "description": "JWT authentication middleware.", "keywords": [ "auth", @@ -33,12 +33,14 @@ ], "main": "./lib", "dependencies": { - "async": "^0.9.0", + "async": "^1.5.0", "express-unless": "^0.3.0", - "jsonwebtoken": "^5.0.0" + "jsonwebtoken": "^7.3.0", + "lodash.set": "^4.0.0" }, "devDependencies": { "mocha": "1.x.x", + "conventional-changelog": "~1.1.0", "restify": "3.x" }, "peerDependencies": { @@ -49,6 +51,6 @@ "node": ">= 0.10.0" }, "scripts": { - "test": "node_modules/.bin/mocha --reporter spec" + "test": "mocha --reporter spec" } } diff --git a/test/jwt.test.js b/test/jwt.test.js index 0dc1f1c6..e91c8a4d 100755 --- a/test/jwt.test.js +++ b/test/jwt.test.js @@ -60,6 +60,14 @@ describe('failure tests', function () { }); }); + it('should next if authorization header is not Bearer and credentialsRequired is false', function() { + req.headers = {}; + req.headers.authorization = 'Basic foobar'; + restifyjwt({secret: 'shhhh', credentialsRequired: false})(req, res, function(err) { + assert.ok(typeof err === 'undefined'); + }); + }); + it('should throw if authorization header is not well-formatted jwt', function() { req.headers = {}; req.headers.authorization = 'Bearer wrongjwt'; @@ -69,6 +77,15 @@ describe('failure tests', function () { }); }); + it('should throw if jwt is an invalid json', function() { + req.headers = {}; + req.headers.authorization = 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.yJ1c2VybmFtZSI6InNhZ3VpYXIiLCJpYXQiOjE0NzEwMTg2MzUsImV4cCI6MTQ3MzYxMDYzNX0.foo'; + restifyjwt({secret: 'shhhh'})(req, res, function(err) { + assert.ok(err); + assert.equal(err.body.code, 'InvalidCredentials'); + }); + }); + it('should throw if authorization header is not valid jwt', function() { var secret = 'shhhhhh'; var token = jwt.sign({foo: 'bar'}, secret); @@ -78,7 +95,6 @@ describe('failure tests', function () { restifyjwt({secret: 'different-shhhh'})(req, res, function(err) { assert.ok(err); assert.equal(err.body.code, 'InvalidCredentials'); - assert.equal(err.we_cause.message, 'invalid signature'); }); }); @@ -91,7 +107,6 @@ describe('failure tests', function () { restifyjwt({secret: 'shhhhhh', audience: 'not-expected-audience'})(req, res, function(err) { assert.ok(err); assert.equal(err.body.code, 'InvalidCredentials'); - assert.equal(err.we_cause.message, 'jwt audience invalid. expected: not-expected-audience'); }); }); @@ -104,7 +119,6 @@ describe('failure tests', function () { restifyjwt({secret: 'shhhhhh'})(req, res, function(err) { assert.ok(err); assert.equal(err.body.code, 'InvalidCredentials'); - assert.equal(err.we_cause.message, 'jwt expired'); }); }); @@ -117,7 +131,6 @@ describe('failure tests', function () { restifyjwt({secret: 'shhhhhh', issuer: 'http://wrong'})(req, res, function(err) { assert.ok(err); assert.equal(err.body.code, 'InvalidCredentials'); - assert.equal(err.we_cause.message, 'jwt issuer invalid. expected: http://wrong'); }); }); @@ -154,7 +167,6 @@ describe('failure tests', function () { restifyjwt({secret: secret})(req,res, function(err) { assert.ok(err); assert.equal(err.body.code, 'InvalidCredentials'); - assert.equal(err.we_cause.message, 'invalid token'); }); }); @@ -175,6 +187,17 @@ describe('work tests', function () { }); }); + it('should work with nested properties', function() { + var secret = 'shhhhhh'; + var token = jwt.sign({foo: 'bar'}, secret); + + req.headers = {}; + req.headers.authorization = 'Bearer ' + token; + restifyjwt({secret: secret, requestProperty: 'auth.token'})(req, res, function() { + assert.equal('bar', req.auth.token.foo); + }); + }); + it('should work if authorization header is valid with a buffer secret', function() { var secret = new Buffer('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'base64'); var token = jwt.sign({foo: 'bar'}, secret); @@ -197,22 +220,38 @@ describe('work tests', function () { }); }); - it('should work if no authorization header and credentials are not required', function() { - req = {}; - restifyjwt({secret: 'shhhh', credentialsRequired: false})(req, res, function(err) { - assert(typeof err === 'undefined'); + it('should set resultProperty if option provided', function() { + var secret = 'shhhhhh'; + var token = jwt.sign({foo: 'bar'}, secret); + + req = { }; + res = { }; + req.headers = {}; + req.headers.authorization = 'Bearer ' + token; + restifyjwt({secret: secret, resultProperty: 'locals.user'})(req, res, function() { + assert.equal('bar', res.locals.user.foo); + assert.ok(typeof req.user === 'undefined'); }); }); - it('should work if token is expired and credentials are not required', function() { + it('should ignore userProperty if resultProperty option provided', function() { var secret = 'shhhhhh'; - var token = jwt.sign({foo: 'bar', exp: 1382412921}, secret); + var token = jwt.sign({foo: 'bar'}, secret); + req = { }; + res = { }; req.headers = {}; req.headers.authorization = 'Bearer ' + token; - restifyjwt({ secret: secret, credentialsRequired: false })(req, res, function(err) { + restifyjwt({secret: secret, userProperty: 'auth', resultProperty: 'locals.user'})(req, res, function() { + assert.equal('bar', res.locals.user.foo); + assert.ok(typeof req.auth === 'undefined'); + }); + }); + + it('should work if no authorization header and credentials are not required', function() { + req = {}; + restifyjwt({secret: 'shhhh', credentialsRequired: false})(req, res, function(err) { assert(typeof err === 'undefined'); - assert(typeof req.user === 'undefined') }); }); @@ -223,6 +262,19 @@ describe('work tests', function () { }); }); + it('should produce a stack trace that includes the failure reason', function() { + var req = {}; + var token = jwt.sign({foo: 'bar'}, 'secretA'); + req.headers = {}; + req.headers.authorization = 'Bearer ' + token; + + restifyjwt({secret: 'secretB'})(req, res, function(err) { + var index = err.stack.indexOf('InvalidCredentialsError') + assert.equal(index, 0, "Stack trace didn't include 'invalid signature' message.") + }); + + }); + it('should work with a custom getToken function', function() { var secret = 'shhhhhh'; var token = jwt.sign({foo: 'bar'}, secret);