Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor code in /lib to conform to ESLint (JavaScript the Good Parts) #213

Merged
merged 4 commits into from
Nov 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
[![NSP Status](https://nodesecurity.io/orgs/dwyl/projects/1047e39b-0d4a-45ff-af65-c04afc41fc20/badge)](https://nodesecurity.io/orgs/dwyl/projects/1047e39b-0d4a-45ff-af65-c04afc41fc20)
[![Build Status](https://travis-ci.org/dwyl/hapi-auth-jwt2.svg "Build Status = Tests Passing")](https://travis-ci.org/dwyl/hapi-auth-jwt2)
[![codecov.io Code Coverage](https://img.shields.io/codecov/c/github/dwyl/hapi-auth-jwt2.svg?maxAge=2592000)](https://codecov.io/github/dwyl/hapi-auth-jwt2?branch=master)
[![JavaScript Style Guide: Good Parts](https://img.shields.io/badge/code%20style-goodparts-brightgreen.svg?style=flat)](https://github.com/dwyl/goodparts "JavaScript The Good Parts")
[![Code Climate](https://codeclimate.com/github/dwyl/hapi-auth-jwt2/badges/gpa.svg "No Nasty Code")](https://codeclimate.com/github/dwyl/hapi-auth-jwt2)
[![HAPI 15.0.3](http://img.shields.io/badge/hapi-15.0.3-brightgreen.svg "Latest Hapi.js")](http://hapijs.com)
[![Node.js Version](https://img.shields.io/node/v/hapi-auth-jwt2.svg?style=flat "Node.js 10 & 12 and io.js latest both supported")](http://nodejs.org/download/)
Expand Down Expand Up @@ -675,3 +676,7 @@ to improve (_security_) it was *ignored* for _weeks_ ... an *authentication* plu
is a ***no-go*** for us; **security** ***matters***!) If you spot _any_
issue in ***hapi-auth-jwt2*** please create an issue: https://github.com/dwyl/hapi-auth-jwt2/issues
so we can get it _resolved_ ASAP!

_Aparently, `.some` people like it..._:

[![https://nodei.co/npm/hapi-auth-jwt2.png?downloads=true&downloadRank=true&stars=true](https://nodei.co/npm/hapi-auth-jwt2.png?downloads=true&downloadRank=true&stars=true)](https://www.npmjs.com/package/hapi-auth-jwt2)
47 changes: 32 additions & 15 deletions lib/extract.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,53 @@
'use strict';

var Cookie = require('cookie'); // highly popular decoupled cookie parser

/**
* customOrDefaultKey is a re-useable method to determing if the developer
* using the plugin has defined a custom key for extractin the JWT
* @param {Object} options - the options passed in when registering the plugin
* @param {String} key - name of the key e.g `urlKey` see: https://git.io/vXbJN
* @param {String} _default - the default key used if no custom is defined.
* @returns {String} key - the custom key or default key.
*/
function customOrDefaultKey (options, key, _default) {
return options[key] === false
|| typeof options[key] === 'string' ? options[key] : _default;
}

/**
* Extract the JWT from URL, Auth Header or Cookie
* @param {Object} request - standard hapi request object inclduing headers
* @param {Object} options - the configuration options defined by the person
* using the plugin. this includes relevant keys. (see docs in Readme)
* @returns {String} token - the raw JSON Webtoken or `null` if invalid
*/

module.exports = function (request, options) {
module.exports = function extract (request, options) {
// The key holding token value in url or cookie defaults to token
var urlKey = options.urlKey === false || typeof options.urlKey === 'string' ? options.urlKey : 'token';
var cookieKey = options.cookieKey === false || typeof options.cookieKey === 'string' ? options.cookieKey : 'token';
var headerKey = options.headerKey === false || typeof options.headerKey === 'string' ? options.headerKey : 'authorization';
var auth;
var auth, token;
var cookieKey = customOrDefaultKey(options, 'cookieKey', 'token');
var headerKey = customOrDefaultKey(options, 'headerKey', 'authorization');
var urlKey = customOrDefaultKey(options, 'urlKey', 'token');
var pattern = new RegExp(options.tokenType + '\\s+([^$]+)', 'i');

if(urlKey && request.query[urlKey]) { // tokens via url: https://github.com/dwyl/hapi-auth-jwt2/issues/19
if (urlKey && request.query[urlKey]) { // tokens via url: https://github.com/dwyl/hapi-auth-jwt2/issues/19
auth = request.query[urlKey];
} // JWT tokens in cookie: https://github.com/dwyl/hapi-auth-jwt2/issues/55
else if (headerKey && request.headers[headerKey]) {
} else if (headerKey && request.headers[headerKey]) {
if (typeof options.tokenType === 'string') {
var token = request.headers[headerKey].match(new RegExp(options.tokenType + '\\s+([^$]+)', 'i'));
token = request.headers[headerKey].match(pattern);
auth = token === null ? null : token[1];
} else {
auth = request.headers[headerKey];
}
}
else if (cookieKey && request.headers.cookie) {
} // JWT tokens in cookie: https://github.com/dwyl/hapi-auth-jwt2/issues/55
} else if (cookieKey && request.headers.cookie) {
auth = Cookie.parse(request.headers.cookie)[cookieKey];
}

// strip pointless "Bearer " label & any whitespace > http://git.io/xP4F
return auth ? auth.replace(/Bearer/gi, '').replace(/ /g, '') : null;
};

module.exports.isValid = function basicChecks (token) {
module.exports.isValid = function isValid (token) {
// rudimentary check for JWT validity see: http://git.io/xPBn for JWT format
return token.split('.').length === 3;
return token.split('.').length === 3;
};
218 changes: 132 additions & 86 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
var Boom = require('boom'); // error handling https://github.com/hapijs/boom
var assert = require('assert');
var JWT = require('jsonwebtoken'); // https://github.com/docdis/learn-json-web-tokens
var extract = require('./extract'); // extract token from Auth Header, URL or Coookie
var pkg = require('../package.json');
'use strict';

var Boom = require('boom'); // error handling https://github.com/hapijs/boom
var assert = require('assert');
var JWT = require('jsonwebtoken'); // https://github.com/docdis/learn-json-web-tokens
var extract = require('./extract'); // extract token from Auth Header, URL or Coookie
var pkg = require('../package.json');
var internals = {}; // Declare internals >> see: http://hapijs.com/styleguide

exports.register = function (server, options, next) {
Expand All @@ -14,136 +16,180 @@ exports.register.attributes = { // hapi requires attributes for a plugin.
pkg: pkg // See: http://hapijs.com/tutorials/plugins
};

/**
* isFunction checks if a given value is a function.
* @param {Object} functionToCheck - the object we want to confirm is a function
* @returns {Boolean} - true if the functionToCheck is a function. :-)
*/
internals.isFunction = function (functionToCheck) {
var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
var getType = {};

return functionToCheck
&& getType.toString.call(functionToCheck) === '[object Function]';
};

internals.isArray = function (functionToCheck) {
var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Array]';
var getType = {};

return functionToCheck
&& getType.toString.call(functionToCheck) === '[object Array]';
};
// function throwBoomError (ctx) {
// return Boom[ctx.errorType](ctx.message, ctx.scheme, ctx.attributes);
// }

internals.implementation = function (server, options) {
assert(options, 'options are required for jwt auth scheme'); // pre-auth checks
assert(options.validateFunc || options.verifyFunc, 'validateFunc OR verifyFunc function is required!')
assert(options.validateFunc
|| options.verifyFunc, 'validateFunc OR verifyFunc function is required!');

// allow custom error raising or default to Boom if no errorFunc is defined
var raiseError = function(errorType, message, scheme, attributes) {
function raiseError (errorType, message, scheme, attributes) {
var errorContext = {
errorType: errorType,
message: message,
scheme: scheme,
attributes: attributes
};
var _errorType = errorType; // copies of params
var _message = message; // so we can over-write them below
var _scheme = scheme; // without a linter warning
var _attributes = attributes; // if you know a better way please PR!

if (options.errorFunc && internals.isFunction(options.errorFunc)) {
var errorContext = {
errorType: errorType,
message: message,
scheme: scheme,
attributes: attributes,
};
errorContext = options.errorFunc(errorContext);

if (errorContext) {
errorType = errorContext.errorType;
message = errorContext.message;
scheme = errorContext.scheme;
attributes = errorContext.attributes;
_errorType = errorContext.errorType;
_message = errorContext.message;
_scheme = errorContext.scheme;
_attributes = errorContext.attributes;
}
}
return Boom[errorType](message, scheme, attributes);
};

return Boom[_errorType](_message, _scheme, _attributes);
}

return {
authenticate: function (request, reply) {

var token = extract(request, options); // extract token from Header, Cookie or Query param

var tokenType = options.tokenType || 'Token'; //
var token = extract(request, options); // extract token Header/Cookie/Query
var tokenType = options.tokenType || 'Token'; // see: https://git.io/vXje9
var decoded, keyFunc;

if (!token) {
return reply(raiseError('unauthorized', null, tokenType));
}

if (!extract.isValid(token)) { // quick check for validity of token format
return reply(raiseError('unauthorized', 'Invalid token format', tokenType));
return reply(raiseError('unauthorized',
'Invalid token format', tokenType));
} // verification is done later, but we want to avoid decoding if malformed
request.auth.token = token; // keep encoded JWT available in the request lifecycle
request.auth.token = token; // keep encoded JWT available in the request
// otherwise use the same key (String) to validate all JWTs
var decoded;

try {
decoded = JWT.decode(token, { complete: options.complete || false });
}
catch(e) { // request should still FAIL if the token does not decode.
return reply(raiseError('unauthorized', 'Invalid token format', tokenType));
} catch (e) { // request should still FAIL if the token does not decode.
return reply(raiseError('unauthorized',
'Invalid token format', tokenType));
}

if(options.key && typeof options.validateFunc === 'function') {
// if the keyFunc is a function it allows dynamic key lookup see: https://github.com/dwyl/hapi-auth-jwt2/issues/16
var keyFunc = (internals.isFunction(options.key)) ? options.key : function (decoded, callback) { callback(null, options.key); };
if (options.key && typeof options.validateFunc === 'function') {
// if keyFunc is function allow dynamic key lookup: https://git.io/vXjvY
keyFunc = (internals.isFunction(options.key))
? options.key : function (decoded_token, callback) {
return callback(null, options.key);
};

keyFunc(decoded, function (err, key, extraInfo) {
var verifyOptions = options.verifyOptions || {};
var keys = (internals.isArray(key)) ? key : [key];
var keysTried = 0;

if (err) {
return reply(raiseError('wrap', err));
}
if (extraInfo) {
request.plugins[pkg.name] = { extraInfo: extraInfo };
}
var verifyOptions = options.verifyOptions || {};
var keys = (internals.isArray(key)) ? key : [ key ];
var keysTried = 0;

keys.some(function (key) {
JWT.verify(token, key, verifyOptions, function (err, decoded) {
if (err) {
keysTried++;
if (keysTried >= keys.length) {
return reply(raiseError('unauthorized', 'Invalid token', tokenType), null, { credentials: null });
keys.some(function (k) { // itterate through one or more JWT keys
JWT.verify(token, k, verifyOptions,
function (verify_err, verify_decoded) {
if (verify_err) {
keysTried++;
if (keysTried >= keys.length) {
return reply(raiseError('unauthorized',
'Invalid token', tokenType), null, { credentials: null });
}
// There are still other keys that might work

// return false;
} else { // see: http://hapijs.com/tutorials/auth for validateFunc signature
return options.validateFunc(verify_decoded, request,
function (validate_err, valid, credentials) { // bring your own checks
if (validate_err) {
return reply(raiseError('wrap', validate_err));
}
if (!valid) {
reply(raiseError('unauthorized',
'Invalid credentials', tokenType), null,
{ credentials: credentials || verify_decoded });
} else {
reply.continue({
credentials: credentials || verify_decoded,
artifacts: token
});
}

return false;
});
}
// There are still other keys that might work

return false;
}
else { // see: http://hapijs.com/tutorials/auth for validateFunc signature
options.validateFunc(decoded, request, function (err, valid, credentials) { // bring your own checks
if (err) {
return reply(raiseError('wrap', err));
}
else if (!valid) {
return reply(raiseError('unauthorized', 'Invalid credentials', tokenType), null, { credentials: credentials || decoded });
}
else {
return reply.continue({ credentials: credentials || decoded, artifacts: token });
}
});
}
});
})
});

return false;
});

return true;
}); // END keyFunc
} // END check for (secret) key and validateFunc
else { // see: https://github.com/dwyl/hapi-auth-jwt2/issues/130
options.verifyFunc(decoded, request, function(err, valid, credentials) {
if (err) {
return reply(raiseError('wrap', err));
}
else if (!valid) {
return reply(raiseError('unauthorized', 'Invalid credentials', tokenType), null, { credentials: decoded });
} else {
return reply.continue({ credentials: credentials || decoded, artifacts: token });
}
});
} else { // see: https://github.com/dwyl/hapi-auth-jwt2/issues/130
return options.verifyFunc(decoded, request,
function (verify_error, valid, credentials) {
if (verify_error) {
return reply(raiseError('wrap', verify_error));
}
if (!valid) {
reply(raiseError('unauthorized', 'Invalid credentials',
tokenType), null, { credentials: decoded });
} else {
reply.continue({
credentials: credentials || decoded,
artifacts: token
});
}

return true;
});
}
// else { // warn the person that the plugin requires either a validateFunc or verifyFunc!
// return reply(Boom.notImplemented('please specify a hapi-auth-jwt2 validateFunc or verifyFunc.', 'Token'), null, { credentials: decoded });
// }

return true;
},
response: function(request, reply) {
if(options.responseFunc && typeof options.responseFunc === 'function') {
options.responseFunc(request, reply, function(err) {
response: function (request, reply) {
if (options.responseFunc && typeof options.responseFunc === 'function') {
options.responseFunc(request, reply, function (err) {
if (err) {
return reply(raiseError('wrap', err));
}
else {
reply(raiseError('wrap', err));
} else {
reply.continue();
}
})
}
else {
});
} else {
reply.continue();
}

return true;
}
};
};
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
},
"devDependencies": {
"aguid": "^1.0.4",
"hapi": "^15.0.3",
"goodparts": "^1.2.0",
"hapi": "^15.2.0",
"istanbul": "^0.4.5",
"jshint": "^2.9.3",
"pre-commit": "^1.1.3",
Expand All @@ -64,11 +65,12 @@
"test": "istanbul cover ./node_modules/tape/bin/tape ./test/*.test.js | node_modules/tap-spec/bin/cmd.js",
"coverage": "istanbul cover ./node_modules/tape/bin/tape ./test/*.test.js && istanbul check-coverage --statements 100 --functions 100 --lines 100 --branches 100",
"jshint": "./node_modules/jshint/bin/jshint -c .jshintrc --exclude-path .gitignore .",
"lint": "node_modules/.bin/goodparts lib",
"start": "node example/server.js",
"report": "open coverage/lcov-report/index.html"
},
"pre-commit": [
"jshint",
"coverage"
"coverage",
"jshint"
]
}