diff --git a/.jshintrc b/.jshintrc index 6bc5b66..f1eb799 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,4 +1,14 @@ { - "esversion": 6, - "node": true -} \ No newline at end of file + "asi" : true, + "esversion" : 6, + "node" : true, + "sub" : true, + "strict": true, + "white": true, + "unused": true, + "eqeqeq": true, + "maxparams": 10, + "maxdepth": 10, + "maxstatements": 25, + "maxcomplexity": 10 +} diff --git a/.travis.yml b/.travis.yml index b5a6ade..4f537e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,13 @@ language: node_js node_js: -- '6.14' \ No newline at end of file +- '6.14' +- '8' +- '10' +- '12' +#jobs: +# include: +# - stage: test +# script: npm test +# - +# script: echo "this is done" +# if: TRAVIS_EVENT == 'cron' diff --git a/accesscontrol/index.js b/accesscontrol/index.js index b9e0cfb..3ab0e83 100644 --- a/accesscontrol/index.js +++ b/accesscontrol/index.js @@ -7,7 +7,7 @@ var debug = require('debug')('plugin:accesscontrol'); var util = require("util"); const dns = require('dns'); -module.exports.init = function (config, logger, stats) { +module.exports.init = function (config /*, logger, stats */) { var allow; var deny; @@ -19,13 +19,13 @@ module.exports.init = function (config, logger, stats) { function checkAccessControlInfo(sourceIP) { if (config === null) debug('WARNING: insufficient information to run accesscontrol'); else if (config.allow === null && config.deny === null) debug('WARNING: insufficient information to run accesscontrol'); - else if (config.allow != null) { + else if (config.allow !== null) { debug ('allow list: ' + util.inspect(config.allow, 2, true)); if (scanIP(config.allow, sourceIP)) { allow = true; } } - else if (config.deny != null) { + else if (config.deny !== null) { debug ('deny list: ' + util.inspect(config.deny, 2, true)); if (scanIP(config.deny, sourceIP)) { debug ('deny incoming message'); @@ -51,12 +51,14 @@ module.exports.init = function (config, logger, stats) { * for each list in the allow and deny, make sure they are proper * IPv4 addresses */ + /* never used function validateIPList(list) { list.forEach(function(entry){ if (!checkIsIPV4(entry)) return false; }); return true; } + */ function scanIP(list, sourceIP) { @@ -64,9 +66,9 @@ module.exports.init = function (config, logger, stats) { //no wildcard for (var i=0; i < list.length; i++) { //no wildcard - if (list[i].indexOf('*') == -1 && list[i] == sourceIP) { + if (list[i].indexOf('*') === -1 && list[i] === sourceIP) { return true; - } else if (list[i].indexOf('*') != -1) { //contains wildcard + } else if (list[i].indexOf('*') !== -1) { //contains wildcard var listOctets = list[i].split('.'); if (octetCompare(listOctets, sourceOctets)) return true; } @@ -83,9 +85,9 @@ module.exports.init = function (config, logger, stats) { var compare = false; for (var i=0; i < listOctets.length; i++) { //debug('list ' + listOctets[i] + ' sourceOctets ' + sourceOctets[i]); - if (listOctets[i] != '*' && parseInt(listOctets[i]) == parseInt(sourceOctets[i])) { + if (listOctets[i] !== '*' && parseInt(listOctets[i]) === parseInt(sourceOctets[i])) { compare = true; - } else if (listOctets[i] != '*' && parseInt(listOctets[i]) != parseInt(sourceOctets[i])) { + } else if (listOctets[i] !== '*' && parseInt(listOctets[i]) !== parseInt(sourceOctets[i])) { return false; } } diff --git a/accumulate-request/index.js b/accumulate-request/index.js index d162087..5aeebba 100644 --- a/accumulate-request/index.js +++ b/accumulate-request/index.js @@ -13,7 +13,7 @@ * high load or with a large number of concurrent requests. So this plugin * should only be used when it is known that request/response bodies are small. */ -module.exports.init = function(config, logger, stats) { +module.exports.init = function(/*config, logger, stats*/) { function accumulate(req, data) { if (!req._chunks) req._chunks = []; req._chunks.push(data); diff --git a/accumulate-response/index.js b/accumulate-response/index.js index 1ff26a2..b864aaf 100644 --- a/accumulate-response/index.js +++ b/accumulate-response/index.js @@ -13,7 +13,7 @@ * high load or with a large number of concurrent requests. So this plugin * should only be used when it is known that request/response bodies are small. */ -module.exports.init = function(config, logger, stats) { +module.exports.init = function(/*config, logger, stats*/) { function accumulate(res, data) { if (!res._chunks) res._chunks = []; diff --git a/analytics/index.js b/analytics/index.js index 77c8c2b..28eeb02 100644 --- a/analytics/index.js +++ b/analytics/index.js @@ -1,8 +1,8 @@ 'use strict'; -var debug = require('debug')('plugin:analytics'); +//var debug = require('debug')('plugin:analytics'); var volos = require('volos-analytics-apigee'); -module.exports.init = function(config, logger, stats) { +module.exports.init = function(config, logger /*, stats */) { config.finalizeRecord = function finalizeRecord(req, res, record, cb) { if (res.proxy) { @@ -10,13 +10,13 @@ module.exports.init = function(config, logger, stats) { //can distinguish between healthcheck calls and regular apis calls. var proxyPath = req.url.split('?')[0]; if (config.proxyPath) { - if (config.proxyPath == proxyPath) { + if (config.proxyPath === proxyPath) { record.apiproxy = res.proxy.name + "-health"; record.apiproxy_revision = res.proxy.revision; } } else if (config.relativePath) { var relativePath = "/" + proxyPath.split('/')[2]; - if (config.relativePath == relativePath) { + if (config.relativePath === relativePath) { record.apiproxy = res.proxy.name + "-health"; record.apiproxy_revision = res.proxy.revision; } diff --git a/apikeys/index.js b/apikeys/index.js index 8abecdc..47e523e 100644 --- a/apikeys/index.js +++ b/apikeys/index.js @@ -14,7 +14,7 @@ var _ = require("lodash"); const PRIVATE_JWT_VALUES = ["application_name", "client_id", "api_product_list", "iat", "exp"]; const SUPPORTED_DOUBLE_ASTERIK_PATTERN = "**"; const SUPPORTED_SINGLE_ASTERIK_PATTERN = "*"; -const SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN = "/"; +// const SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN = "/"; // ?? this has yet to be used in any module. const acceptAlg = ["RS256"]; @@ -44,12 +44,13 @@ module.exports.init = function(config, logger, stats) { //this flag will enable check against resource paths only productOnly = config.hasOwnProperty("productOnly") ? config.productOnly : false; //if local proxy is set, ignore proxies - if (process.env.EDGEMICRO_LOCAL_PROXY == "1") { + if (process.env.EDGEMICRO_LOCAL_PROXY === "1") { productOnly = true; } //leaving rest of the code same to ensure backward compatibility - if (apiKey = req.headers[apiKeyHeaderName]) { + apiKey = req.headers[apiKeyHeaderName] + if ( apiKey ) { if (!keepApiKey) { delete(req.headers[apiKeyHeaderName]); // don't pass this header to target } @@ -177,7 +178,7 @@ module.exports.init = function(config, logger, stats) { return { onrequest: function(req, res, next) { - if (process.env.EDGEMICRO_LOCAL == "1") { + if (process.env.EDGEMICRO_LOCAL === "1") { debug ("MG running in local mode. Skipping OAuth"); next(); } else { @@ -268,7 +269,7 @@ const checkIfAuthorized = module.exports.checkIfAuthorized = function checkIfAut } else { // if(apiproxy.includes(SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN)){ // } - matchesProxyRules = urlPath == apiproxy; + matchesProxyRules = urlPath === apiproxy; } } @@ -291,7 +292,7 @@ function getPEM(decodedToken, keys) { var i = 0; debug("jwk kid " + decodedToken.headerObj.kid); for (; i < keys.length; i++) { - if (keys.kid == decodedToken.headerObj.kid) { + if (keys.kid === decodedToken.headerObj.kid) { break; } } @@ -299,26 +300,37 @@ function getPEM(decodedToken, keys) { return rs.KEYUTIL.getPEM(publickey); } -function sendError(req, res, next, logger, stats, code, message) { - - switch (code) { - case "invalid_request": +function setResponseCode(res,code) { + switch ( code ) { + case 'invalid_request': { res.statusCode = 400; break; - case "access_denied": + } + case 'access_denied':{ res.statusCode = 403; break; - case "invalid_token": - case "missing_authorization": - case "invalid_request": + } + case 'invalid_token': + case 'missing_authorization': + case 'invalid_authorization': { res.statusCode = 401; break; - case "gateway_timeout": + } + case 'gateway_timeout': { res.statusCode = 504; break; - default: + } + default: { res.statusCode = 500; + break; + } } +} + + +function sendError(req, res, next, logger, stats, code, message) { + + setResponseCode(res,code) var response = { error: code, diff --git a/cors/index.js b/cors/index.js index 7cb11a1..d101e49 100644 --- a/cors/index.js +++ b/cors/index.js @@ -1,7 +1,7 @@ 'use strict' var debug = require('debug')('plugin:cors'); -module.exports.init = function(config, logger, stats) { +module.exports.init = function(config /*, logger, stats */) { var methods = config['cors-methods'] || 'GET, PUT, POST, DELETE, PATCH, OPTIONS'; var maxAge = config['cors-max-age'] || '3628800'; @@ -16,7 +16,7 @@ module.exports.init = function(config, logger, stats) { if (origin) accessControlAllowOriginValue = origin; else accessControlAllowOriginValue = req.headers['origin']; - if(req.method == 'OPTIONS') { + if(req.method === 'OPTIONS') { res.setHeader('Access-Control-Allow-Origin', accessControlAllowOriginValue); res.setHeader('Access-Control-Allow-Methods', methods); res.setHeader('Access-Control-Allow-Max-Age', maxAge); diff --git a/eurekaclient/index.js b/eurekaclient/index.js index 907621a..fc53cfd 100644 --- a/eurekaclient/index.js +++ b/eurekaclient/index.js @@ -5,15 +5,15 @@ */ var debug = require('debug')('plugin:eurekeclient'); -var util = require('util'); +//var util = require('util'); var os = require('os'); const port = process.env.PORT || 8000; const Eureka = require('eureka-js-client').Eureka; -module.exports.init = function (config, logger, stats) { +module.exports.init = function (config /*, logger, stats */) { - const lookup = config.servicemap; + //const lookup = config.servicemap; config.instance.hostName = os.hostname(); debug('local hostName: ' + config.instance.hostName); @@ -49,7 +49,7 @@ module.exports.init = function (config, logger, stats) { function getAppName(url) { for (var index in config.lookup) { - if (url.includes(config.lookup[index].uri) || url == config.lookup[index].uri) { + if (url.includes(config.lookup[index].uri) || url === config.lookup[index].uri) { return { app: config.lookup[index].app, secure: config.lookup[index].secure @@ -63,8 +63,8 @@ module.exports.init = function (config, logger, stats) { var instances = client.getInstancesByAppId(app); for (var index in instances) { - if (instances[index].status == "UP") { - return (secure == true) ? {"hostName": instances[index].hostName, "port": instances[index].securePort["$"]} : {"hostName": instances[index].hostName, "port":instances[index].port["$"]}; + if (instances[index].status === "UP") { + return (secure === true) ? {"hostName": instances[index].hostName, "port": instances[index].securePort["$"]} : {"hostName": instances[index].hostName, "port":instances[index].port["$"]}; } } return ""; @@ -87,7 +87,7 @@ module.exports.init = function (config, logger, stats) { req.targetSecure = false; } } else { - console.warn("Target enpoint from Eureka not found"); + console.warn("Target endpoint from Eureka not found"); } next(); } diff --git a/extauth/index.js b/extauth/index.js index 0b4ecbd..98ae2c6 100644 --- a/extauth/index.js +++ b/extauth/index.js @@ -44,7 +44,7 @@ module.exports.init = function(config, logger, stats) { console.log(err); } else { debug("loaded public keys"); - if (keyType == 'jwk') { + if (keyType === 'jwk') { debug("keyType is jwk"); publickeys = JSON.parse(body); } else { @@ -55,9 +55,9 @@ module.exports.init = function(config, logger, stats) { }); function getJWK(kid) { - if (publickeys.keys && publickeys.keys.constructor == Array) { + if (publickeys.keys && publickeys.keys.constructor === Array) { for (var i = 0; i < publickeys.keys.length; i++) { - if (publickeys.keys[i].kid == kid) { + if (publickeys.keys[i].kid === kid) { return publickeys.keys[i]; } } @@ -91,7 +91,7 @@ module.exports.init = function(config, logger, stats) { try { var jwtpayload = authHeaderRegex.exec(req.headers['authorization']); - if (!jwtpayload || jwtpayload.length < 2) { + if ( !(jwtpayload) || (jwtpayload.length < 2) ) { debug("ERROR - JWT Token Missing in Auth header"); delete(req.headers['authorization']); delete(req.headers['x-api-key']); @@ -100,10 +100,10 @@ module.exports.init = function(config, logger, stats) { } } else { var jwtdecode = JWS.parse(jwtpayload[1]); - if (jwtdecode.headerObj) { + if ( jwtdecode.headerObj ) { var kid = jwtdecode.headerObj.kid; debug("Found jwt kid: " + kid); - if (keyType != 'jwk') { + if ( keyType !== 'jwk' ) { debug("key type is PEM"); isValid = validateJWT(publickeys, jwtpayload[1], exp); if (isValid) { @@ -122,7 +122,7 @@ module.exports.init = function(config, logger, stats) { return sendError(req, res, next, logger, stats, 'invalid_token'); } } - } else if (!kid && keyType == 'jwk') { + } else if (!kid && keyType === 'jwk') { debug("ERROR - JWT Missing kid in header"); delete(req.headers['authorization']); delete(req.headers['x-api-key']); @@ -184,26 +184,37 @@ module.exports.init = function(config, logger, stats) { }; } -function sendError(req, res, next, logger, stats, code, message) { - switch (code) { - case 'invalid_request': +function setResponseCode(res,code) { + switch ( code ) { + case 'invalid_request': { res.statusCode = 400; break; - case 'access_denied': + } + case 'access_denied':{ res.statusCode = 403; break; + } case 'invalid_token': case 'missing_authorization': - case 'invalid_authorization': + case 'invalid_authorization': { res.statusCode = 401; break; - case 'gateway_timeout': + } + case 'gateway_timeout': { res.statusCode = 504; break; - default: + } + default: { res.statusCode = 500; + break; + } } +} + +function sendError(req, res, next, logger, stats, code, message) { + + setResponseCode(res,code) var response = { error: code, diff --git a/futures/oauth-test.js b/futures/oauth-test.js new file mode 100644 index 0000000..66225b4 --- /dev/null +++ b/futures/oauth-test.js @@ -0,0 +1,391 @@ +const oauth = require('../oauth/index'); +const oauthv2 = require('../oauthv2/index'); +// +const assert = require('assert'); +const denv = require('dotenv'); +denv.config(); + + +var oauthConfiigDefaults = { + "authorization-header" : "authorization", + "api-key-header" : 'x-api-key', + "keep-authorization-header" : false, + "cacheKey" : false, + "gracePeriod" : 0, + "allowOAuthOnly" : false, + "allowAPIKeyOnly" : false, + "productOnly" : false, + "tokenCache" : false, + "tokenCacheSize" : 100, + "allowNoAuthorization" : false, + "jwk_keys" : undefined, + "request" : undefined +} + + +var default_onrequest_cb = (err) => { + assert.ok(!(err instanceof Error)); + done(); +}; + +var generic_req = { + token: { + application_name: '0e7762f4-ea67-4cc1-ae4a-21598c35b18f', + api_product_list: ['EdgeMicroTestProduct'] + } +} + +var generic_res = { + headers: {}, + setHeader: (key, val) => { + res.headers[key] = val; + } +} + + +// var generic_req_params = [generic_req, generic_res, default_onrequest_cb]; + + + +describe('oauth plugins', function() { + var plugin = null; + + //this.timout(0) + + before(() => { + // + + }) + + beforeEach(() => { + // environment variables.... + process.env.EDGEMICRO_LOCAL_PROXY = "0" + process.env.EDGEMICRO_LOCAL = "0" + process.env.EDGEMICRO_OPENTRACE = false + // + }); + + + after((done) => { + if ( plugin ) plugin.shutdown(); + done(); + }) + + // unit tests originally in oauth/test/oauth.test + + var config = { + "verify_api_key_url":"https://sfeldmanmicro-test.apigee.net/edgemicro-auth/verifyApiKey", + "product_to_proxy":{"EdgeMicroTestProduct":["edgemicro_weather"]}, + "product_to_api_resource":{"EdgeMicroTestProduct":["/hello/blah/*/foo*","/hello/some/**","/hello/blah"]} + }; + var config2 = { + "verify_api_key_url":"https://sfeldmanmicro-test.apigee.net/edgemicro-auth/verifyApiKey", + "product_to_proxy":{"EdgeMicroTestProduct":["edgemicro_weather"]}, + "product_to_api_resource":{"EdgeMicroTestProduct":[]} + }; + var config3 = { + "verify_api_key_url":"https://sfeldmanmicro-test.apigee.net/edgemicro-auth/verifyApiKey", + "product_to_proxy":{"EdgeMicroTestProduct":["edgemicro_weather"]}, + "product_to_api_resource":{"EdgeMicroTestProduct":["/blah/*/foo*","/some/**","blah"]} + }; + + var proxy = {name:'edgemicro_weather',base_path:'/hello'} + var token = {api_product_list:['EdgeMicroTestProduct']} + + var auths = [oauth, oauthv2] + + auths.forEach(authMod => { + + + var tests = authMod.tests; + + var authObj = null; + + it('initialize the base class without error',(done) => { + if ( authMod == oauth ) { + + var logger = {}; + var stats = {}; + // + authObj = tests.initTest('oauth',oauthConfiigDefaults, logger, stats) + } else { + var logger = {}; + var stats = {}; + // + authObj = tests.initTest('oauthv2',oauthConfiigDefaults, logger, stats) + } + + done(); + }) + + it('will not initialize without a well formed config',(done) => { + var checkObj = { + 'a' : 1, + 'b' : 2, + 'c' : 3, + 'd' : 4, + 'e' : 5, + 'f' : 6, + } + // + var result = tests.test_objectWithoutProperties(checkObj,['a','c','e']) + // + assert(result['a'] === undefined) + assert(result['b'] === 2) + // + result = tests.test_objectWithoutProperties(checkObj,['a','c','f']) + // + assert(result['d'] === 4) + assert(result['f'] === undefined) + + done(); + }); + + + it('will not initialize without a well formed config',(done) => { + var logger = {}; + var stats = {}; + + var myplugin = authMod.init(undefined, logger, stats); + assert(myplugin === undefined) + + myplugin = authMod.init(null, logger, stats); + assert(myplugin === undefined) + + done(); + }) + + it('exposes an onrequest handler', (done) => { + var logger = {}; + var stats = {}; + // + var pluginT = authMod.init(oauthConfiigDefaults, logger, stats); + assert.ok(pluginT.onrequest); + // + done(); + }); + + it('runs in local mode',(done) => { + // + process.env.EDGEMICRO_LOCAL = "1" + var logger = {}; + var stats = {}; + + var req = null; + var res = null; + + var myplugin = authMod.init(oauthConfiigDefaults, logger, stats); + myplugin.onrequest(req,res,()=>{ + process.env.EDGEMICRO_LOCAL = "0" + assert(true) + done(); + }) + + }) + + it('takes a default config and bad req and res',(done) => { + // + var logger = {}; + var stats = {}; + var req = null; + var res = null; + // + var cb_called = false; + // + var cb = () => { + cb_called = true; + assert(false) + done(); + } + // + try { + var pluginT = authMod.init(oauthConfiigDefaults, logger, stats); + pluginT.onrequest(req,res,cb) + if ( !cb_called ) { + assert(true); + } + req = {} + res = {} + pluginT.onrequest(req,res,cb) + if ( !cb_called ) { + assert(true); + done(); + } + // + } catch(e) { + console.log(e); + assert(false) + done() + } + + }) + + it('req and res are empty and default config ', (done) => { + // + var logger = {}; + var stats = {}; + // + var req = { + headers : {} + }; + var res = {}; + // + process.env.EDGEMICRO_LOCAL_PROXY = "1" + // + var cb_called = false; + // + var cb = () => { + cb_called = true; + assert(true) + done(); + } + // + try { + var pluginT = authMod.init(oauthConfiigDefaults, logger, stats); + pluginT.onrequest(req,res,cb) + if ( !cb_called ) { + assert(false); + done(); + } + // + } catch(e) { + console.log(e); + assert(false) + done() + } + + }) + + + + }) + + + // should be identical for these tests + var modules = { "oauth" : oauth, "oauthv2" : oauthv2 } + for (var name in modules) { + + const logger = {} + const stats = {} + + var tests = modules[name].tests; + + describe(name, function() { + + var package = modules[name] + + + it('checkIfAuthorized',function (done) { + + var authObj = tests.initTest('oauth',config, logger, stats) + + var contains; + contains = authObj.checkIfAuthorized('/hello',proxy,token); + assert(!contains) + contains = authObj.checkIfAuthorized('/hello/blah',proxy,token); + assert(contains) + contains = authObj.checkIfAuthorized('/hello/blah/somerule/foosomething',proxy,token); + assert(contains) + contains = authObj.checkIfAuthorized('/hello/blah/somerule/ifoosomething',proxy,token); + assert(!contains) + contains = authObj.checkIfAuthorized('/hello/some/somerule/foosomething',proxy,token); + assert(contains) + done() + }) + + it('checkIfAuthorizedNoConfig',function (done) { + + var authObj = tests.initTest('oauth',config2, logger, stats) + + var contains; + contains = authObj.checkIfAuthorized('/hello',proxy,token); + assert(contains) + contains = authObj.checkIfAuthorized('/hello/blah',proxy,token); + assert(contains) + contains = authObj.checkIfAuthorized('/hello/blah/somerule/foosomething',proxy,token); + assert(contains) + contains = authObj.checkIfAuthorized('/hello/blah/somerule/ifoosomething',proxy,token); + assert(contains) + contains = authObj.checkIfAuthorized('/hello/some/somerule/foosomething',proxy,token); + assert(contains) + done() + }) + + it('checkIfAuthorized3',function (done) { + + var authObj = tests.initTest('oauth',config3, logger, stats) + + var contains; + contains = authObj.checkIfAuthorized('/hello',proxy,token); + assert(!contains) + contains = authObj.checkIfAuthorized('/hello/blah',proxy,token); + assert(contains) + contains = authObj.checkIfAuthorized('/hello/blah/somerule/foosomething',proxy,token); + assert(contains) + contains = authObj.checkIfAuthorized('/hello/blah/somerule/ifoosomething',proxy,token); + assert(!contains) + contains = authObj.checkIfAuthorized('/hello/some/somerule/foosomething',proxy,token); + assert(contains) + done() + + }) + + + it('exposes an onrequest handler', function() { + var config = {} + var plugin = package.init.apply(null, [config, logger, stats]); + assert.ok(plugin.onrequest); + }); + + it('ejectToken where gracePeriod == 0', function() { + var config = { + allowOAuthOnly: true, + allowNoAuthorization: true, + gracePeriod: 0, + } + + var plugin = package.init.apply(null, [config, logger, stats]) + var cb = (err) => {} + var req = {headers: {}} + var res = {} + plugin.onrequest.apply(null, [req, res, cb]); // called to init vars + + authObj = tests.initTest('oauth',config, logger, stats) + // not expired + var exp = (new Date().getTime() / 1000) + 5 + assert.ok(!authObj.ejectToken(exp), "should not eject") + + // expired + var exp = new Date().getTime() / 1000 - 5 + assert.ok(authObj.ejectToken(exp), "should eject") + }); + + it('ejectToken where gracePeriod != 0', function() { + var config = { + allowOAuthOnly: true, + allowNoAuthorization: true, + gracePeriod: 5, + } + var plugin = package.init.apply(null, [config, logger, stats]) + + var cb = (err) => {} + var req = {headers: {}} + var res = {} + plugin.onrequest.apply(null, [req, res, cb]); // called to init vars + + authObj = tests.initTest('oauth',config, logger, stats) + // not expired + var exp = (new Date().getTime() / 1000) + 5 + assert.ok(!authObj.ejectToken(exp), "should not eject") + + // expired, inside of grace period + var exp = new Date().getTime() / 1000 - 3 + assert.ok(!authObj.ejectToken(exp), "should not eject") + + // expired, outside of grace period + var exp = new Date().getTime() / 1000 - 6 + assert.ok(authObj.ejectToken(exp), "should eject") + }); + }) + } +}); diff --git a/futures/oauth/index.js b/futures/oauth/index.js new file mode 100644 index 0000000..678acf8 --- /dev/null +++ b/futures/oauth/index.js @@ -0,0 +1,263 @@ +'use strict'; + +var debug = require('debug')('plugin:oauth'); + +var basicAuth = require('../lib/basicAuth'); +// +var fs = require('fs'); +var path = require('path'); +var requestLib = require('request'); + + +class apiKeyAuthorization extends basicAuth.BasicAuthorizerPlugin { + + constructor(config, logger, stats, authType) { + // + super(config, logger, stats, authType); + + this.request = config.request ? requestLib.defaults(config.request) : requestLib; + + this.cacheKey = false; + + this.enableCache(); + + this.updateConfig2(config); + } + + updateConfig2(config) { + super.updateConfig(); + // + this.apiKeyHeaderName = config.hasOwnProperty('api-key-header') ? config['api-key-header'] : 'x-api-key'; + this.cacheKey = config.hasOwnProperty('cacheKey') ? config.cacheKey : false; + + //support for enabling oauth or api key only + this.oauth_only = config.hasOwnProperty('allowOAuthOnly') ? config.allowOAuthOnly : false; + this.apikey_only = config.hasOwnProperty('allowAPIKeyOnly') ? config.allowAPIKeyOnly : false; + } + + // -------- -------- -------- -------- -------- -------- + + missingApiKey(req,res) { + debug('missing api key'); + return this.sendError(req, res, this.next, this.logger, this.stats, 'invalid_authorization', 'Missing API Key header'); + } + + apikeyGatewayTimeout(req, res, message) { + debug('verify apikey gateway timeout'); + return this.sendError(req, res, this.next, this.logger, this.stats, 'gateway_timeout', message); + } + + // -------- -------- -------- -------- -------- -------- + + // + exchangeApiKeyForToken(req, res, apiKey) { + + this.setApiKey(apiKey) + + var cacheControl = req.headers['cache-control'] || 'no-cache'; + var cacheAllowed = ( !cacheControl || (cacheControl && cacheControl.indexOf('no-cache') < 0) ); + + if ( this.cacheEnabled || cacheAllowed ) { // caching is allowed + // + this.cache.read(apiKey, function(err, value) { + if ( err ) { + // + } + if ( value ) { + if ( (Date.now() / 1000) < value.exp ) { // not expired yet (token expiration is in seconds) + debug('api key cache hit', apiKey); + return this.authorize(req, res, value); + } else { + this.cache.remove(apiKey); + debug('api key cache expired', apiKey); + this.requestApiKeyJWT(req, res); + } + } else { + debug('api key cache miss', apiKey); + this.requestApiKeyJWT(req, res); + } + }); + } else { + this.requestApiKeyJWT(req, res); + } + + } + + + ifCachingCache(req,decodedToken) { + if ( (this.apiKey !== undefined) && this.apiKey ) { + var cacheControl = req.headers['cache-control'] || 'no-cache'; + if ( this.cacheEnabled || this.cacheKey || (!cacheControl || (cacheControl && cacheControl.indexOf('no-cache') < 0)) ) { // caching is allowed + // default to now (in seconds) + 30m if not set + decodedToken.exp = decodedToken.exp || +(((Date.now() / 1000) + 1800).toFixed(0)); + this.cache.store(this.apiKey, decodedToken,decodedToken.exp); + debug('api key cache store', this.apiKey); + } else { + debug('api key cache skip', this.apiKey); + } + } + } + + + setApiKeyOptions() { + + var api_key_options = { + url: this.config.verify_api_key_url, + method: 'POST', + json: { + 'apiKey': this.apiKey + }, + headers: { + 'x-dna-api-key': this.apiKey + } + }; + + var agentOptions = this.config.agentOptions; + + if ( agentOptions ) { + if ( agentOptions.requestCert ) { + api_key_options.requestCert = true; + if ( agentOptions.cert && agentOptions.key ) { + var keyPath = path.resolve(agentOptions.key) + api_key_options.key = fs.readFileSync(keyPath, 'utf8'); + var certPath = path.resolve(agentOptions.cert) + api_key_options.cert = fs.readFileSync(certPath,'utf8'); + if ( agentOptions.ca ) { + var caPath = path.resolve(agentOptions.ca) + api_key_options.ca = fs.readFileSync(caPath, 'utf8'); + } + } else if ( agentOptions.pfx ) { + var pfxPath = path.resolve(agentOptions.pfx) + api_key_options.pfx = fs.readFileSync(pfxPath); + } + if ( agentOptions.rejectUnauthorized ) { + api_key_options.rejectUnauthorized = true; + } + if ( agentOptions.secureProtocol ) { + api_key_options.secureProtocol = true; + } + if ( agentOptions.ciphers ) { + api_key_options.ciphers = agentOptions.ciphers; + } + if ( agentOptions.passphrase ) api_key_options.passphrase = agentOptions.passphrase; + } + } + + return(api_key_options) + } + + requestApiKeyJWT(req, res) { + + if ( !(this.config.verify_api_key_url) ) return this.sendError(req, res, 'invalid_request', 'API Key Verification URL not configured'); + + var api_key_options = this.setApiKeyOptions(); + + var request = this.request; + var self = this; + + request(api_key_options, function(err, keyRes, body) { + if ( err ) { + return this.apikeyGatewayTimeout(req, res, err.message) + } + if ( keyRes.statusCode !== 200 ) { + return this.accessDenied(req, res, "api key:: " + keyRes.message) + } + self.verify(body, req, res); + }); + + + } +} + + + +module.exports.init = function(config, logger, stats) { + + if ( config === undefined || !config ) return(undefined); + // + var authObj = new apiKeyAuthorization(config, logger, stats,'oauth'); + + var middleware = function(req, res, next) { + + if ( !req || !res ) return(-1); // need to check bad args + if ( !req.headers ) return(-1); // or throw -- means callers are bad + + authObj.updateConfig2(config) + // + authObj.setNext(next); + + // PARAMETERS FROM REQUEST OBJECT + // prefer an authorization header if available + var authHeader = req.headers[this.authHeaderName] + var haveAuthHeader = (authHeader !== undefined) && authHeader; + + // attempt to get a valid api key + var apiKey = req.headers[this.apiKeyHeaderName] + var haveApiKey = (apiKey !== undefined) && apiKey; + if ( !(haveApiKey) ) { // try alternative + if ( req.reqUrl && req.reqUrl.query ) { + apiKey = req.reqUrl.query[this.apiKeyHeaderName]; + haveApiKey = (apiKey !== undefined) && apiKey; + } + } + + // + //support for enabling oauth or api key only + if ( authObj.oauth_only ) { + if ( haveAuthHeader ) { + var [matches,code] = authObj.matchHeader(authHeader) + if ( !(matches) ) return(code) + } else { + // no header give it a chance to bipass + if ( authObj.allowNoAuthorization ) { + return next(); + } else { + return( authObj.missingAuthorization(req, res) ); + } + } + } else if ( authObj.apikey_only ) { + if ( !(haveApiKey) ) { + return( authObj.missingApiKey(req,res) ); + } + } + + //leaving rest of the code same to ensure backward compatibility + if ( !(haveAuthHeader) || authObj.allowAPIKeyOnly ) { + if ( haveApiKey ) { + // API KEY ALTERNATIVE + authObj.exchangeApiKeyForToken(req, res, apiKey); + // + } else if ( authObj.allowNoAuthorization ) { + return next(); + } else { + return( authObj.missingAuthorization(req, res) ); + } + } else { + return( authObj.authFromToken(req,res,authHeader) ) + } + } // end of middleware + + return { + + onrequest: function(req, res, next) { + if ( process.env.EDGEMICRO_LOCAL === "1" ) { + debug ("MG running in local mode. Skipping OAuth"); + next(); + } else { + middleware(req, res, next); + } + }, + + shutdown() { + // tests are needing shutdowns to remove services that keep programs running, etc. + } + + }; + +} // end of init + +// from the product name(s) on the token, find the corresponding proxy +// then check if that proxy is one of the authorized proxies in bootstrap +// +module.exports.checkIfAuthorized = basicAuth.checkIfAuthorized; +module.exports.tests = basicAuth.tests diff --git a/futures/oauth/package.json b/futures/oauth/package.json new file mode 100644 index 0000000..49354e5 --- /dev/null +++ b/futures/oauth/package.json @@ -0,0 +1,18 @@ +{ + "name": "edgemicro-plugin-oauth", + "version": "1.1.0", + "private": true, + "description": "A plugin for authenticating JWT tokens", + "dependencies": { + "debug": "^2.2.0", + "jsonwebtoken": "^5.0.1", + "lodash": "^4.17.5", + "request": "^2.65.0" + }, + "devDependencies": { + "chai": "^2.3.0", + "mocha": "^2.2.4", + "pem": "^1.7.2", + "supertest": "^1.0.1" + } +} diff --git a/futures/oauth/test/module.js b/futures/oauth/test/module.js new file mode 100644 index 0000000..23fcf3b --- /dev/null +++ b/futures/oauth/test/module.js @@ -0,0 +1,65 @@ +var assert = require('assert') +var oauth = require('../index.js') +var config = { + "verify_api_key_url":"https://sfeldmanmicro-test.apigee.net/edgemicro-auth/verifyApiKey", + "product_to_proxy":{"EdgeMicroTestProduct":["edgemicro_weather"]}, + "product_to_api_resource":{"EdgeMicroTestProduct":["/hello/blah/*/foo*","/hello/some/**","/hello/blah"]} +}; +var config2 = { + "verify_api_key_url":"https://sfeldmanmicro-test.apigee.net/edgemicro-auth/verifyApiKey", + "product_to_proxy":{"EdgeMicroTestProduct":["edgemicro_weather"]}, + "product_to_api_resource":{"EdgeMicroTestProduct":[]} +}; +var config3 = { + "verify_api_key_url":"https://sfeldmanmicro-test.apigee.net/edgemicro-auth/verifyApiKey", + "product_to_proxy":{"EdgeMicroTestProduct":["edgemicro_weather"]}, + "product_to_api_resource":{"EdgeMicroTestProduct":["/blah/*/foo*","/some/**","blah"]} +}; + +var proxy = {name:'edgemicro_weather',base_path:'/hello'} + +var token = {api_product_list:['EdgeMicroTestProduct']} +describe('test oauth',function(){ + it('checkIfAuthorized',function (done) { + var contains; + contains = oauth.checkIfAuthorized(config,'/hello',proxy,token); + assert(!contains) + contains = oauth.checkIfAuthorized(config,'/hello/blah',proxy,token); + assert(contains) + contains = oauth.checkIfAuthorized(config,'/hello/blah/somerule/foosomething',proxy,token); + assert(contains) + contains = oauth.checkIfAuthorized(config,'/hello/blah/somerule/ifoosomething',proxy,token); + assert(!contains) + contains = oauth.checkIfAuthorized(config,'/hello/some/somerule/foosomething',proxy,token); + assert(contains) + done() + }) + it('checkIfAuthorizedNoConfig',function (done) { + var contains; + contains = oauth.checkIfAuthorized(config2,'/hello',proxy,token); + assert(contains) + contains = oauth.checkIfAuthorized(config2,'/hello/blah',proxy,token); + assert(contains) + contains = oauth.checkIfAuthorized(config2,'/hello/blah/somerule/foosomething',proxy,token); + assert(contains) + contains = oauth.checkIfAuthorized(config2,'/hello/blah/somerule/ifoosomething',proxy,token); + assert(contains) + contains = oauth.checkIfAuthorized(config2,'/hello/some/somerule/foosomething',proxy,token); + assert(contains) + done() + }) + it('checkIfAuthorized3',function (done) { + var contains; + contains = oauth.checkIfAuthorized(config3,'/hello',proxy,token); + assert(!contains) + contains = oauth.checkIfAuthorized(config3,'/hello/blah',proxy,token); + assert(contains) + contains = oauth.checkIfAuthorized(config3,'/hello/blah/somerule/foosomething',proxy,token); + assert(contains) + contains = oauth.checkIfAuthorized(config3,'/hello/blah/somerule/ifoosomething',proxy,token); + assert(!contains) + contains = oauth.checkIfAuthorized(config3,'/hello/some/somerule/foosomething',proxy,token); + assert(contains) + done() + }) +}) diff --git a/futures/oauth/test/oauth.test.js b/futures/oauth/test/oauth.test.js new file mode 100644 index 0000000..0a1f323 --- /dev/null +++ b/futures/oauth/test/oauth.test.js @@ -0,0 +1,481 @@ + + +'use strict'; + +var helper = require('./test-helper'); +var config = helper.config(); + +var debug = require('debug')('plugin:oauth:test'); +var http = require('http'); +var request = require('supertest'); +var chai = require('chai'); +var should = chai.should(); +var pem = require('pem'); +var jwt = require('jsonwebtoken'); +var util = require('util'); + +chai.config.includeStack = true; +chai.config.showDiff = true; + +describe('oauth', function() { + + var privateKey, publicKey; + + before(function(done) { + var options = { + selfSigned: true, + days: 1 + }; + pem.createCertificate(options, function(err, keys) { + if (err) { return done(err); } + + privateKey = keys.serviceKey; + publicKey = keys.certificate; + + config.oauth.public_key = publicKey; + config.oauth.allowNoAuthorization = false; + config.oauth.allowInvalidAuthorization = false; + + done(); + }); + }); + + var servers, proxy; + + before(function(done) { + config.edgemicro.plugins.sequence = ['oauth']; + helper.startServers(config, function(err, s) { + if (err) { return done(err); } + servers = s; + proxy = servers.proxy; + done(); + }); + }); + + after(function() { + servers.close(); + }); + + + describe('no authorization', function() { + + it('should fail when allowNoAuthorization is false', function(done) { + + config.oauth.allowNoAuthorization = false; + config.oauth.allowInvalidAuthorization = false; + request(proxy) + .get('/') + .expect(401) + .end(function(err, res) { + if (err) { return done(err); } + should.not.exist(res.body.fromTarget); + done(); + }); + }); + + it('should succeed when allowNoAuthorization is true', function(done) { + + config.oauth.allowNoAuthorization = true; + config.oauth.allowInvalidAuthorization = false; + request(proxy) + .get('/') + .expect(200) + .end(function(err, res) { + if (err) { return done(err); } + should.exist(res.body.fromTarget); + should.not.exist(res.headers.authorization); + + should.not.exist(res.body.headers['authorization']); + should.not.exist(res.body.headers['x-authorization-claims']); + done(); + }); + }); + }); + + describe('bad bearer token', function() { + + it('should fail with missing bearer token', function(done) { + + config.oauth.allowNoAuthorization = false; + config.oauth.allowInvalidAuthorization = false; + request(proxy) + .get('/') + .set('Authorization', 'Bearer') + .expect(400) + .end(function(err, res) { + if (err) { return done(err); } + should.not.exist(res.body.fromTarget); + done(); + }); + }); + + it('should fail when allowInvalidAuthorization is false', function(done) { + + config.oauth.allowNoAuthorization = false; + config.oauth.allowInvalidAuthorization = false; + request(proxy) + .get('/') + .set('Authorization', 'Bearer BadBearerToken') + .expect(401) + .end(function(err, res) { + if (err) { return done(err); } + should.not.exist(res.body.fromTarget); + done(); + }); + }); + + it('should succeed when allowInvalidAuthorization is true', function(done) { + + config.oauth.allowNoAuthorization = false; + config.oauth.allowInvalidAuthorization = true; + request(proxy) + .get('/') + .set('Authorization', 'Bearer BadBearerToken') + .expect(200) + .end(function(err, res) { + if (err) { return done(err); } + should.exist(res.body.fromTarget); + should.not.exist(res.headers.authorization); + + should.not.exist(res.body.headers['authorization']); + should.not.exist(res.body.headers['x-authorization-claims']); + done(); + }); + }); + }); + + describe('good bearer token', function() { + + it('should succeed if valid proxy path', function(done) { + + var options = { algorithm: 'RS256' }; + var payload = { + application_name: 'app', + client_id: 'client', + scopes: ['scope1'], + api_product_list: [ 'Test' ], + test: 'test' + }; + var token = jwt.sign(payload, privateKey, options); + + config.oauth.allowNoAuthorization = false; + config.oauth.allowInvalidAuthorization = false; + request(proxy) + .get('/') + .set('Authorization', 'Bearer ' + token) + .expect(200) + .end(function(err, res) { + if (err) { return done(err); } + should.exist(res.body.fromTarget); + should.not.exist(res.headers.authorization); + + should.not.exist(res.body.headers['authorization']); + should.exist(res.body.headers['x-authorization-claims']); + var claims = JSON.parse(new Buffer(res.body.headers['x-authorization-claims'], 'base64').toString()); + claims.should.have.keys('scopes', 'test'); + + done(); + }); + }); + + it('should fail if missing required proxy path', function(done) { + + var options = { algorithm: 'RS256' }; + var payload = { test: 'test' }; + var token = jwt.sign(payload, privateKey, options); + + config.oauth.allowNoAuthorization = false; + config.oauth.allowInvalidAuthorization = false; + request(proxy) + .get('/') + .set('Authorization', 'Bearer ' + token) + .expect(403) + .end(function(err, res) { + if (err) { return done(err); } + should.not.exist(res.body.fromTarget); + done(); + }); + }); + + it('should fail if expired', function(done) { + + var options = { algorithm: 'RS256', expiresInSeconds: 1 }; + var payload = { test: 'test' }; + var token = jwt.sign(payload, privateKey, options); + + setTimeout(function() { + + config.oauth.allowNoAuthorization = false; + config.oauth.allowInvalidAuthorization = false; + + request(proxy) + .get('/') + .set('Authorization', 'Bearer ' + token) + .expect(403) + .end(function(err, res) { + if (err) { return done(err); } + should.not.exist(res.body.fromTarget); + done(); + }); + }, 1000); + }); + }); + + describe('configured auth header', function() { + + before(function() { + config.oauth.allowNoAuthorization = false; + config.oauth.allowInvalidAuthorization = false; + config.oauth['authorization-header'] = 'x-proxy-auth'; + }); + + after(function() { + delete(config.oauth['authorization-header']); + }); + + it('should succeed if in configured header', function(done) { + + var options = { algorithm: 'RS256' }; + var payload = { + application_name: 'app', + client_id: 'client', + scopes: ['scope1'], + api_product_list: [ 'Test' ], + test: 'test' + }; + var token = jwt.sign(payload, privateKey, options); + + request(proxy) + .get('/') + .set('Authorization', 'Whatever') + .set('x-proxy-auth', 'Bearer ' + token) + .expect(200) + .end(function(err, res) { + if (err) { return done(err); } + should.exist(res.body.fromTarget); + should.not.exist(res.headers.authorization); + + should.exist(res.body.headers['authorization']); + should.exist(res.body.headers['x-authorization-claims']); + var claims = JSON.parse(new Buffer(res.body.headers['x-authorization-claims'], 'base64').toString()); + claims.should.have.keys('scopes', 'test'); + + done(); + }); + }); + + it('should fail if in default header', function(done) { + + var options = { algorithm: 'RS256' }; + var payload = { + application_name: 'app', + client_id: 'client', + scopes: ['scope1'], + api_product_list: [ 'Test' ], + test: 'test' + }; + var token = jwt.sign(payload, privateKey, options); + + request(proxy) + .get('/') + .set('Authorization', 'Bearer ' + token) + .expect(401) + .end(function(err, res) { + if (err) { return done(err); } + should.not.exist(res.body.fromTarget); + done(); + }); + + }); + }); + + describe('apikey', function() { + + var verifier; + + var apiKey = '6gClRCKp0UCOZ8o9Q5S7X88nI5hgizGQ'; + var jwtTokenForApiKey = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhcHBsaWNhdGlvbl9uYW1lIjoiSkRzIEFwcCIsImNsaWVudF9pZCI6IjZnQ2xSQ0twMFV' + + 'DT1o4bzlRNVM3WDg4bkk1aGdpekdRIiwic2NvcGVzIjpbXSwiYXBpX3Byb2R1Y3RfbGlzdCI6WyJ0cmF2ZWwtYXBwIl0sImlhdCI6MTQ0NTU' + + '1MjYxMH0.glQTK-Nh0YFYbWK-pr8nbJuIvt51p6zmLe53CbZ3JLEcG0QjHYcKmUPLPgDOGYfyjnYMUVMMfIDDZlhemy84fKQMGRptCgUfmga' + + '9roLNPKPujhxFXb9GhkQ94KXxm8GChuvjYxn8K7K_nAhnzn4wB84rczvm91ytOwFPCeS_t6KbLS3uMrj6Nj1gITGeZVlm2QLAvUlJ5Abua0t' + + 'OItHj7_nvzHHwClgN9Is1UZ7LW5f747kVWp10t1JbAmubyTP-01TSbaniDGfBCmi0JYOizUFZiMjdSVcZP-tqWvHAsLdQ2T9k4sKG1I1pKcK' + + 'vh3dmS3j8PWhTHIV3FM1x7L5hig'; + var apiPublicKey = + '-----BEGIN CERTIFICATE-----\n' + + 'MIICpDCCAYwCCQDd9JO8DIvnqTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwls\n' + + 'b2NhbGhvc3QwHhcNMTUwNjI1MDMxNTQ5WhcNMTUwNjI2MDMxNTQ5WjAUMRIwEAYD\n' + + 'VQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY\n' + + 'O0JHvd1m/H/VyGmQpuqY/CvZGbx/wRWrLG/YErYl1w30e615hJOBpT1neavXONGw\n' + + '2kuiqjRon8WcWvjrmKSDitul1MhUqddEBC+JhMfZpgCr9axcrFgRxm4JcfrhWoqE\n' + + 'LXAYLo/VXzNCCkGz4SLcp/azpnnPBeTm5m4AJBMW0YrznBQrVMJHGXXvd1b9q5Hp\n' + + 'Ejui9VdIEknhNJ88bjP3Lq7j0efCbkj0IAYNigdbC+eIMbRHVHNyEaioUAU9pYAp\n' + + '2v1tomnwlQbEhD3vVnWJprKWgLGtYf9pmwrffwtDu73gyO/qr+aVNSNlchQ4fOag\n' + + '3xeYKmAUHOl6QpiIocjzAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAG9B1kST84u0\n' + + 'yR5/CNqHO6wx2SiYKOfrhkvKbxsql/u59qxYd6jHPyE4adLkp/PPWz6DN4ucRT4J\n' + + '2UULFD2biRvPK9Ua3J7IKLUMOuVnwU7JFCiAf51e0A9Weqh6L0+ATTkZMddUfKTA\n' + + 'uLdVOBz43EPCizKAjUP39+RFuUhBJazNrXyibE4fP/r6pJ7EB4a57HnvTlNkPZXA\n' + + 'z24Ihg42rqDXgFmNPE6X9p/pSOzA87iGLrOznbFQzTHJg/wLqmPEPVrjFwXWfzoI\n' + + 'CAysSvrNWbfqSIoxxWPpkK+O10m2DqYaQMu3J73p9kgD0gV9KBSW2rMFDg6JnI4G\n' + + 'BDvtgIzP58M=\n' + + '-----END CERTIFICATE-----'; + + before(function(done) { + verifier = http.createServer(function(req, res) { + var key = req.headers['x-dna-api-key']; + if (key === apiKey) { + res.statusCode = 200; + res.end(jwtTokenForApiKey); + } else { + res.statusCode = 401; + res.end(); + } + }); + verifier.listen(function(err) { + if (err) { return done(err); } + config.oauth.verify_api_key_url = 'http://127.0.0.1:' + verifier.address().port; + config.oauth.public_key = apiPublicKey; + config.oauth.product_to_proxy = {}; + config.oauth.product_to_proxy['travel-app'] = ['microgateway_test']; + done(); + }); + }); + + after(function() { + verifier.close(); + }); + + it('in header', function(done) { + request(proxy) + .get('/') + .set('x-api-key', apiKey) + .expect(200) + .end(function(err, res) { + debug(res.body); + if (err) { return done(err); } + done(); + }); + }); + + it('in custom header', function(done) { + config.oauth['api-key-header'] = 'custom-api-key-header'; + request(proxy) + .get('/') + .set('custom-api-key-header', apiKey) + .expect(200) + .end(function(err, res) { + debug(res.body); + delete config.oauth['api-key-header']; + if (err) { return done(err); } + done(); + }); + }); + + it('in url param', function(done) { + request(proxy) + .get('/?x-api-key=' + apiKey) + .expect(200) + .end(function(err, res) { + debug(res.body); + if (err) { return done(err); } + done(); + }); + }); + + it('in custom url param', function(done) { + config.oauth['api-key-header'] = 'custom-api-key-header'; + request(proxy) + .get('/?custom-api-key-header=' + apiKey) + .expect(200) + .end(function(err, res) { + debug(res.body); + delete config.oauth['api-key-header']; + if (err) { return done(err); } + done(); + }); + }); + + it('in header (invalid)', function(done) { + request(proxy) + .get('/') + .set('x-api-key', (apiKey + 'x')) + .expect(403) + .end(function(err, res) { + debug(res.body); + if (err) { return done(err); } + done(); + }); + }); + + it('in url (invalid)', function(done) { + request(proxy) + .get('/?x-api-key=' + (apiKey + 'x')) + .expect(403) + .end(function(err, res) { + debug(res.body); + if (err) { return done(err); } + done(); + }); + }); + + it('expiration', function(done) { + config.oauth.public_key = publicKey; + + var payload = {}; + var options = { algorithm: 'RS256' }; + + var savedToken = jwtTokenForApiKey; + var decoded = jwt.decode(jwtTokenForApiKey); + Object.keys(decoded).forEach(function(key) { payload[key] = decoded[key] }); + options.expiresIn = -1; // expired already + jwtTokenForApiKey = jwt.sign(payload, privateKey, options); + + request(proxy) + .get('/') + .set('x-api-key', apiKey) + .set('cache-control', 'no-cache') + .expect(403) // expired + .end(function(err, res) { + debug('expired: ' + util.inspect(res.body)); + if (err) { return done(err); } + + options.expiresIn = 30; // expires 30s from now + jwtTokenForApiKey = jwt.sign(payload, privateKey, options); + + request(proxy) // should succeed now + .get('/') + .set('x-api-key', apiKey) + .set('cache-control', 'no-cache') + .expect(200) + .end(function(err, res) { + debug('restored: ' + util.inspect(res.body)); + jwtTokenForApiKey = savedToken; // restore token + if (err) { return done(err); } + done(); + }); + }); + }); + + it('keep auth header', function(done) { + config.oauth.keepAuthHeader = true; + config.oauth.allowNoAuthorization = true; + config.oauth.allowInvalidAuthorization = false; + + request(proxy) + .get('/') + .expect(200) + .end(function(err, res) { + if (err) { return done(err); } + should.exist(res.body.fromTarget); + should.exist(res.headers.authorization); + + should.not.exist(res.body.headers['authorization']); + should.not.exist(res.body.headers['x-authorization-claims']); + done(); + }); + + }); + + }); + +}); diff --git a/test-helper/echo-chunks.js b/futures/oauth/test/test-helper/echo-chunks.js similarity index 100% rename from test-helper/echo-chunks.js rename to futures/oauth/test/test-helper/echo-chunks.js diff --git a/test-helper/index.js b/futures/oauth/test/test-helper/index.js similarity index 98% rename from test-helper/index.js rename to futures/oauth/test/test-helper/index.js index 00c983d..dca2ee6 100644 --- a/test-helper/index.js +++ b/futures/oauth/test/test-helper/index.js @@ -11,6 +11,10 @@ var debug = require('debug')('gateway:test'); var http = require('http'); var url = require('url'); + +var gateway = require('../../gateway/lib/gateway'); + + var chai = require('chai'); chai.config.includeStack = true; chai.config.showDiff = true; @@ -118,7 +122,6 @@ module.exports.startServers = function startServers(config, cb) { }; function startGateway(config, targets, cb) { - var gateway = require('../../gateway/lib/gateway'); replaceVolosApigeeWithMemory(gateway, 'analytics'); config.edgemicro.port = 0; // to let gateway start on a port of its choosing diff --git a/test-helper/package.json b/futures/oauth/test/test-helper/package.json similarity index 100% rename from test-helper/package.json rename to futures/oauth/test/test-helper/package.json diff --git a/test-helper/test/accumulate-transform.test.js b/futures/oauth/test/test-helper/test/accumulate-transform.test.js similarity index 100% rename from test-helper/test/accumulate-transform.test.js rename to futures/oauth/test/test-helper/test/accumulate-transform.test.js diff --git a/test-helper/test/certificate.pem b/futures/oauth/test/test-helper/test/certificate.pem similarity index 100% rename from test-helper/test/certificate.pem rename to futures/oauth/test/test-helper/test/certificate.pem diff --git a/test-helper/test/key.pem b/futures/oauth/test/test-helper/test/key.pem similarity index 100% rename from test-helper/test/key.pem rename to futures/oauth/test/test-helper/test/key.pem diff --git a/test-helper/test/passthru.test.js b/futures/oauth/test/test-helper/test/passthru.test.js similarity index 100% rename from test-helper/test/passthru.test.js rename to futures/oauth/test/test-helper/test/passthru.test.js diff --git a/test-helper/test/sequence.test.js b/futures/oauth/test/test-helper/test/sequence.test.js similarity index 100% rename from test-helper/test/sequence.test.js rename to futures/oauth/test/test-helper/test/sequence.test.js diff --git a/futures/oauthv2/index.js b/futures/oauthv2/index.js new file mode 100644 index 0000000..d702c06 --- /dev/null +++ b/futures/oauthv2/index.js @@ -0,0 +1,63 @@ +'use strict'; + +var debug = require('debug')('plugin:oauthv2'); + +var basicAuth = require('../lib/basicAuth') +// + +module.exports.init = function(config, logger, stats) { + + if ( config === undefined || !config ) return(undefined); + + var authObj = new basicAuth.BasicAuthorizerPlugin(config, logger, stats,'oauthv2'); + + // middleware + var middleware = (req, res, next) => { + + if ( !req || !res ) return(-1); // need to check bad args + if ( !req.headers ) return(-1); // or throw -- means callers are bad + + authObj.updateConfig(config) + // + authObj.setNext(next); + + // PARAMETERS FROM REQUEST OBJECT + var authHeader = req.headers[this.authHeaderName] + var haveAuthHeader = (authHeader !== undefined) && authHeader; + + if ( !haveAuthHeader ) { + if ( config.allowNoAuthorization ) { + return next(); + } else { + return(authObj.missingAuthorization(req, res, next)); + } + } else { + return( authObj.authFromToken(req,res,authHeader) ) + } + + } // end of middleware + + return { + + onrequest: function(req, res, next) { + if (process.env.EDGEMICRO_LOCAL === "1") { + debug ("MG running in local mode. Skipping OAuth"); + next(); + } else { + middleware(req, res, next); + } + }, + + shutdown() { + // tests are needing shutdowns to remove services that keep programs running, etc. + } + + }; + +} // end of init + +// from the product name(s) on the token, find the corresponding proxy +// then check if that proxy is one of the authorized proxies in bootstrap +// +module.exports.checkIfAuthorized = basicAuth.checkIfAuthorized; +module.exports.tests = basicAuth.tests diff --git a/futures/oauthv2/package.json b/futures/oauthv2/package.json new file mode 100644 index 0000000..4619143 --- /dev/null +++ b/futures/oauthv2/package.json @@ -0,0 +1,12 @@ +{ + "name": "edgemicro-plugin-oauthv2", + "version": "1.0.0", + "private": true, + "description": "A plugin for authenticating JWT tokens", + "dependencies": { + "debug": "^3.1.0", + "jsrsasign": "^7.2.0", + "lodash": "^4.17.10", + "request": "^2.87.0" + } +} diff --git a/header-uppercase/index.js b/header-uppercase/index.js index 9a77e5f..a1c1a88 100644 --- a/header-uppercase/index.js +++ b/header-uppercase/index.js @@ -3,7 +3,7 @@ var debug = require('debug')('plugin:header-uppercase'); // required -module.exports.init = function(config, logger, stats) { +module.exports.init = function(/*config, logger, stats*/) { var counter = 0; diff --git a/healthcheck/index.js b/healthcheck/index.js index b5b16d1..22db877 100644 --- a/healthcheck/index.js +++ b/healthcheck/index.js @@ -6,11 +6,11 @@ var portastic = require('portastic') const HEALTHCHECK_URL = '/healthcheck'; -module.exports.init = function(config, logger, stats) { +module.exports.init = function(config /*, logger, stats */) { return { onrequest: function(req, res, next) { var healthcheck_url = config['healthcheck_url'] || HEALTHCHECK_URL - if(healthcheck_url == req.url) { + if(healthcheck_url === req.url) { var statusCode = (toobusy() ? 503 : 200) debug(statusCode) var healthInfo = { @@ -20,13 +20,13 @@ module.exports.init = function(config, logger, stats) { pid: process.pid } //Check for cloud foundry healthcheck - if(req.targetPort != '' && process.env.EDGEMICRO_DECORATOR){ + if(req.targetPort !== '' && process.env.EDGEMICRO_DECORATOR){ var port = req.targetPort portastic.test(port) .then(function(isOpen){ if (isOpen){ statusCode = 500 - var errorDescription = 'Application is not running on specified applicaiton port: ' + port + var errorDescription = 'Application is not running on specified application port: ' + port healthInfo.decoratorError = errorDescription debug(errorDescription) debug(statusCode) diff --git a/invalidHeader/index.js b/invalidHeader/index.js index f3e3824..ebbc0fb 100644 --- a/invalidHeader/index.js +++ b/invalidHeader/index.js @@ -4,12 +4,12 @@ var debug = require('debug')('plugin:header-clean'); const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/g // strips bad characters from header values -module.exports.init = function(config, logger, stats) { +module.exports.init = function(/*config, logger, stats*/) { return { onrequest: function(req, res, next) { Object.keys(req.headers).forEach(function(key) { var val = req.headers[key] - if (typeof val == 'string') { + if (typeof val === 'string') { var newVal = val.replace(headerCharRegex, '') if (debug.enabled && val.match(headerCharRegex)) { debug('Cleaned header %s. Old: %s. New: %s', key, val, newVal); diff --git a/json2xml/index.js b/json2xml/index.js index 8bff3d8..f9a4c4f 100644 --- a/json2xml/index.js +++ b/json2xml/index.js @@ -27,9 +27,9 @@ var debug = require('debug')('plugin:json2xml'); var js2xmlparser = require("js2xmlparser"); //library to convert xml to json var parseString = require("xml2js").parseString; -var util = require("util"); +//var util = require("util"); -module.exports.init = function (config, logger, stats) { +module.exports.init = function (/*config, logger, stats*/) { //initialize the variables to false @@ -107,20 +107,20 @@ module.exports.init = function (config, logger, stats) { // ondata_request: function(req, res, data, next) { debug('plugin ondata_request'); - if (data && data.length > 0 && disable == false) accumulateRequest(req, data); + if (data && data.length > 0 && disable === false) accumulateRequest(req, data); next(null, null); }, // // ondata_response: function(req, res, data, next) { debug('plugin ondata_response'); - if (data && data.length > 0 && disable == false) accumulateResponse(res, data); + if (data && data.length > 0 && disable === false) accumulateResponse(res, data); next(null, null); }, // onend_request: function(req, res, data, next) { debug('plugin onend_request'); - if (data && data.length > 0 && disable == false) accumulateRequest(res, data); + if (data && data.length > 0 && disable === false) accumulateRequest(res, data); var content = null; if(req._chunks && req._chunks.length) { content = Buffer.concat(req._chunks); @@ -153,7 +153,7 @@ module.exports.init = function (config, logger, stats) { // onend_response: function(req, res, data, next) { debug('plugin onend_request'); - if (data && data.length > 0 && disable == false) accumulateResponse(res, data); + if (data && data.length > 0 && disable === false) accumulateResponse(res, data); var contentType = res.getHeader('content-type'); if (contentType && contentType.indexOf("application/xml") > -1) { diff --git a/lib/basicAuth.js b/lib/basicAuth.js new file mode 100644 index 0000000..516ff0f --- /dev/null +++ b/lib/basicAuth.js @@ -0,0 +1,536 @@ +'use strict'; + +var debug = require('debug')('plugin:basicAuth'); + +var url = require('url'); + +// +var rs = require('jsrsasign'); +var JWS = rs.jws.JWS; + +// +const memoredpath = '../third_party/memored/index'; +var map = require(memoredpath); + +function _enableCache() { + return(require(memoredpath)); +} + +map.setup({ + purgeInterval: 10000 +}); + + +// +// +const acceptAlg = ['RS256']; + +const SUPPORTED_DOUBLE_ASTERIK_PATTERN = "**"; +const SUPPORTED_SINGLE_ASTERIK_PATTERN = "*"; +//const SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN = "/"; + +const AUTH_HEADER_REGEX = /Bearer (.+)/; +const PRIVATE_JWT_VALUES = ['application_name', 'client_id', 'api_product_list', 'iat', 'exp']; + +// +const DEFAULT_TOKEN_CACHE_SIZE = 100; +const DEFAULT_HAS_TOKEN_CACHE = false; + + +// from Babel .. then changed +function _objectWithoutProperties(obj, keys) { + var target = {}; + var objkeys = Object.keys(obj); + + var useKeys = objkeys.filter(key => { + return(keys.indexOf(key) < 0) + }); + + var n = useKeys.length; + for ( var i = 0; i < n; i++ ) { + var ky = useKeys[i]; + target[ky] = obj[ky]; + } + + return target; +} + + + +class BasicAuthorizerPlugin { + + // ---- + constructor(config, logger, stats, authType) { + // + this.config = config; + this.logger = logger; + this.stats = stats; + this.authType = authType; + + // should this be done? + for ( var k in config ) { + this[k] = config[k] + } + + this.cacheEnabled = false; + this.authType = authType; + + this.keys = config.jwk_keys ? JSON.parse(config.jwk_keys) : null; + + // + this.acceptField = {}; + this.acceptField.alg = acceptAlg; + // + + this.productOnly = false; + + this.updateConfig(config) + // + } + + + + updateConfig(config) { + + this.code = '200'; + + this.authHeaderName = config.hasOwnProperty('authorization-header') ? config['authorization-header'] : 'authorization'; + this.keepAuthHeader = config.hasOwnProperty('keep-authorization-header') ? config['keep-authorization-header'] : false; + + //token cache settings + this.tokenCache = config.hasOwnProperty('tokenCache') ? config.tokenCache : DEFAULT_HAS_TOKEN_CACHE; + //max number of tokens in the cache + this.tokenCacheSize = config.hasOwnProperty('tokenCacheSize') ? config.tokenCacheSize : DEFAULT_TOKEN_CACHE_SIZE; + + // + var gracePeriod = config.hasOwnProperty('gracePeriod') ? config.gracePeriod : 0; + this.acceptField.gracePeriod = gracePeriod; + this.i_gracePeriod = parseInt(this.acceptField.gracePeriod); + // + + //this flag will enable check against resource paths only + this.productOnly = config.hasOwnProperty('productOnly') ? config.productOnly : false; + //if local proxy is set, ignore proxies + if ( process.env.EDGEMICRO_LOCAL_PROXY === "1" ) { + this.productOnly = true; + } + + this.apiKey = false; + + + } + + setNext(next) { + this.next = next; + } + + setApiKey(apiKey) { + this.apiKey = apiKey; + } + + enableCache() { + this.cacheEnabled = true; + this.cache =_enableCache(); + } + + // this should be in a separate module. This code is being copied from instance to instance. + ejectToken(expTimestampInSeconds) { + var currentTimestampInSeconds = new Date().getTime() / 1000; + var gracePeriod = this.i_gracePeriod; + return currentTimestampInSeconds > (expTimestampInSeconds + gracePeriod); + } + + // -------- -------- -------- -------- -------- -------- + + missingAuthorization(req, res) { + debug('missing_authorization'); + return this.sendError(req, res, this.next, this.logger, this.stats, 'missing_authorization', 'Missing Authorization header'); + } + + invalidAuthorization(req, res) { + debug('Invalid Authorization Header'); + return this.sendError(req, res, this.next, this.logger, this.stats, 'invalid_request', 'Invalid Authorization header'); + } + + invalidToken(req, res) { + debug('invalid token'); + return this.sendError(req, res, this.next, this.logger, this.stats, 'invalid_token','token could not be parsed'); + } + + accessDenied(req, res, message){ + debug('access_denied:: ' + message); + return this.sendError(req, res, this.next, this.logger, this.stats, 'access_denied', message); + } + + + // -------- -------- -------- -------- -------- -------- + + matchHeader(req, res, authHeader) { + // + var header = AUTH_HEADER_REGEX.exec(authHeader); + // + if ( !this.allowInvalidAuthorization ) { + if ( !header || (header.length < 2) ) { + debug('Invalid Authorization Header'); + var code = this.invalidAuthorization(req, res); + return([false,code]) + } + } + + return([true,header]) + } + + tokenFromHeader(req,res,authHeader) { + + if ( (authHeader === undefined) || !(authHeader) ) return(false); + + var [matches,header] = this.matchHeader(req,res,authHeader) + + if ( matches ) { + // + if ( !this.keepAuthHeader ) { + delete(req.headers[this.authHeaderName]); // don't pass this header to target + } + // + var token = ''; + if ( header ) { + token = header[1]; + } + // + return(token) + } + + return(false) + } + + authFromToken(req,res,authHeader) { + var token = this.tokenFromHeader(req,res,authHeader); + if ( token ) { + this.verify(token, req, res); + return(true); + } else { + // another failed authorization... + return(false); + } + } + + validateOutOfCacheToken(decodedToken,oauthtoken) { + var isValid = false; + try { + if ( this.keys ) { + debug('using jwk'); + var pem = this.getPEM(decodedToken, this.keys ); + isValid = JWS.verifyJWT(oauthtoken, pem, this.acceptField); + } else { + debug('validating jwt'); + isValid = JWS.verifyJWT(oauthtoken, this.config.public_key, this.acceptField); + } + } catch (error) { + console.warn('error parsing jwt: ' + oauthtoken); + } + return(isValid) + } + + tryValidationBipass(req, res) { + if ( this.allowInvalidAuthorization ) { + console.warn('ignoring err in verify no valid JWT'); + return this.next(); + } else { + return this.invalidToken(req, res); + } + } + + + // -------- -------- -------- -------- -------- -------- + + + manageTokenExpiration(decodedToken,oauthtoken) { + if ( this.ejectToken(decodedToken.payloadObj.exp) ) { + debug('ejecting token from cache'); + map.remove(oauthtoken); + } + } + + storeOrUpdateToken(tokenvalue,decodedToken) { + if ( (tokenvalue === null) || (tokenvalue === undefined) ) { + map.size(function(err, sizevalue) { + if ( !err && (sizevalue !== null) && (sizevalue < this.tokenCacheSize) ) { + map.store(tokenvalue, tokenvalue, decodedToken.payloadObj.exp); + } else { + debug('too many tokens in cache; ignore storing token'); + } + }); + } + } + + + ifCachingCache(/*req,decodedToken*/) { // override with these parameters + // override + } + + + + // -------- -------- -------- -------- -------- -------- + + + + verify(token, req, res) { + + var next = this.next; + + var isValid = false; + var oauthtoken = (token && token.token) ? token.token : token; + var decodedToken = null; + // + try { + decodedToken = JWS.parse(oauthtoken); + } catch (err) { + if ( this.allowInvalidAuthorization ) { + console.warn('ignoring err'); + return next(); + } else { + return this.invalidToken(req, res); + } + } + // + if ( this.tokenCache === true ) { + debug('token caching enabled') + map.read(oauthtoken, (err, tokenvalue) => { + // + isValid = false; + var cachedTokenMatches = (tokenvalue !== undefined) && (tokenvalue !== null) && (tokenvalue === oauthtoken); + // + if ( !err && cachedTokenMatches ) { + debug('found token in cache'); + isValid = true; + this.manageTokenExpiration(decodedToken,oauthtoken) + } else { + debug('token not found in cache'); + isValid = this.validateOutOfCacheToken(decodedToken,oauthtoken); + if ( !isValid ) { + return(this.tryValidationBipass(req, res)) + } else { + tokenvalue = oauthtoken + } + } + + if ( isValid ) { + // + this.storeOrUpdateToken(tokenvalue,decodedToken) + this.authorize(req, res, decodedToken.payloadObj) + // + } + }); + } else { + debug('token cache not in use'); + isValid = this.validateOutOfCacheToken(decodedToken,oauthtoken); + if ( !isValid ) { + return(this.tryValidationBipass(req, res)) + } else { + this.authorize(req, res, decodedToken.payloadObj); + } + } + } + + + // -------- -------- -------- -------- -------- -------- + + getPEM(decodedToken, keys) { + var i = 0; + debug('jwk kid ' + decodedToken.headerObj.kid); + for (; i < keys.length; i++) { + if (keys.kid === decodedToken.headerObj.kid) { + break; + } + } + var publickey = rs.KEYUTIL.getKey(keys.keys[i]); + return rs.KEYUTIL.getPEM(publickey); + } + + + authorize(req, res, decodedToken) { + // + if ( this.checkIfAuthorized(this.config, req.reqUrl.path, res.proxy, decodedToken) ) { + req.token = decodedToken; + // + var authClaims = _objectWithoutProperties(decodedToken, PRIVATE_JWT_VALUES); + req.headers['x-authorization-claims'] = new Buffer(JSON.stringify(authClaims)).toString('base64'); + // + this.ifCachingCache(req,decodedToken) + // + this.next(); + } else { + return this.accessDenied(req, res, 'authorize: access_denied') + } + // + } + + /// -------- + + checkIfAuthorized(urlPath, proxy, decodedToken) { + + var parsedUrl = url.parse(urlPath); + // + debug('product only: ' + this.productOnly); + // + + if ( !decodedToken.api_product_list ) { + debug('no api product list'); + return false; + } + + // return true if one is found + return decodedToken.api_product_list.some((product) => { + + const validProxyNames = this.config.product_to_proxy[product]; + + if ( !this.productOnly ) { + if ( !validProxyNames ) { + debug('no proxies found for product'); + return false; + } + } + + const apiproxies = this.config.product_to_api_resource[product]; + + var matchesProxyRules = false; // return value + + if ( apiproxies && apiproxies.length ) { + + // an implementation of some... + matchesProxyRules = apiproxies.some( tempApiProxy => { + + urlPath = parsedUrl.pathname; + const apiproxy = tempApiProxy.includes(proxy.base_path) ? + tempApiProxy : + proxy.base_path + (tempApiProxy.startsWith("/") ? "" : "/") + tempApiProxy + + if (apiproxy.endsWith("/") && !urlPath.endsWith("/")) { + urlPath = urlPath + "/"; + } + + if ( apiproxy.includes(SUPPORTED_DOUBLE_ASTERIK_PATTERN) ) { + const regex = apiproxy.replace(/\*\*/gi, ".*") + return(urlPath.match(regex) !== null ) + } else { + if ( apiproxy.includes(SUPPORTED_SINGLE_ASTERIK_PATTERN) ) { + const regex = apiproxy.replace(/\*/gi, "[^/]+"); + return(urlPath.match(regex) !== null ) + } else { + // if(apiproxy.includes(SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN)){ + // } + return(urlPath === apiproxy); + } + } + + }); + + } else { + matchesProxyRules = true + } + + //debug("matches proxy rules: " + matchesProxyRules); + //add pattern matching here + if ( !(this.productOnly) ) + return matchesProxyRules && (validProxyNames.indexOf(proxy.name) >= 0); + else + return matchesProxyRules; + }); + + } // end of checkIfAuthorized + + + setResponseCode(res,code) { + switch ( code ) { + case 'invalid_request': { + res.statusCode = 400; + break; + } + case 'access_denied':{ + res.statusCode = 403; + break; + } + case 'invalid_token': + case 'missing_authorization': + case 'invalid_authorization': { + res.statusCode = 401; + break; + } + case 'gateway_timeout': { + res.statusCode = 504; + break; + } + default: { + res.statusCode = 500; + break; + } + } + } + + + // General error handling for authorization failures. + sendError(req, res, next, logger, stats, code, message) { + + this.setResponseCode(res,code); + + var response = { + error: code, + error_description: message + }; + + debug('auth failure', res.statusCode, code, message ? message : '', req.headers, req.method, req.url); + if ( logger && logger.error && (typeof logger.error === 'function') ) { + logger.error({ + req: req, + res: res + }, 'oauth'); + } + + //opentracing + if ( process.env.EDGEMICRO_OPENTRACE ) { + try { + const traceHelper = require('../microgateway-core/lib/trace-helper'); + traceHelper.setChildErrorSpan('oauth', req.headers); + } catch (err) {} + } + // + + if ( !res.finished ) { + try { + res.setHeader('content-type', 'application/json'); + } catch (e) { + console.warn("oath response object lacks setHeader") + } + } + + try { + res.end(JSON.stringify(response)); + } catch (e) { + console.warn("oath response object is not supplied by runtime") + } + + try { + stats.incrementStatusCount(res.statusCode); + } catch (e) { + console.warn("oath stats object is not supplied by runtime") + } + + next(code, message); + return code; + } + +} // end of BasicAuthorizerPlugin class definition. + + + + +module.exports.BasicAuthorizerPlugin = BasicAuthorizerPlugin; + + +module.exports.tests = { + initTest : (authType, config, logger, stats) => { + var authObj = new BasicAuthorizerPlugin(config, logger, stats, authType); + return(authObj); + }, + test_objectWithoutProperties : _objectWithoutProperties +} diff --git a/monitor/index.js b/monitor/index.js index bd61fee..04b4c95 100644 --- a/monitor/index.js +++ b/monitor/index.js @@ -4,12 +4,13 @@ */ var debug = require('debug')('plugin:monitor'); -var path = require('path'); +// var path = require('path'); const memoredpath = '../third_party/memored/index'; var cache = require(memoredpath); const lynx = require('lynx'); +const os = require('os'); -module.exports.init = function(config, logger, stats) { +module.exports.init = function(config /*, logger, stats */) { var host = config.host || 'localhost'; var port = config.port || 8125; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 3277dcb..575f620 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -228,6 +228,12 @@ "tweetnacl": "^0.14.3" } }, + "beeper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", + "dev": true + }, "bluebird": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", @@ -472,7 +478,7 @@ "dotenv": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", - "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==" + "integrity": "sha1-7TEMFltOipe7dFsKnZnDG9pWZEA=" }, "ecc-jsbn": { "version": "0.1.2", @@ -773,6 +779,23 @@ "har-schema": "^2.0.0" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -838,6 +861,12 @@ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true }, + "irregular-plurals": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz", + "integrity": "sha1-LKmwM2UREYVUEvFr5dd8YqRYp2Y=", + "dev": true + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -1024,6 +1053,62 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "jshint-stylish": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jshint-stylish/-/jshint-stylish-2.2.1.tgz", + "integrity": "sha1-JCCCosA1rgP9gQROBXDMQgjPbmE=", + "dev": true, + "requires": { + "beeper": "^1.1.0", + "chalk": "^1.0.0", + "log-symbols": "^1.0.0", + "plur": "^2.1.0", + "string-length": "^1.0.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -1160,6 +1245,57 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "requires": { + "chalk": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -1652,6 +1788,15 @@ "find-up": "^3.0.0" } }, + "plur": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", + "integrity": "sha1-dIJFLBoPUI4+NE6uwxLJHCncZVo=", + "dev": true, + "requires": { + "irregular-plurals": "^1.0.0" + } + }, "portastic": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/portastic/-/portastic-1.0.1.tgz", @@ -1854,10 +1999,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-3.0.1.tgz", + "integrity": "sha1-cgrAElFaJS+R+w3S6ZpWpw1s8Hg=" }, "set-blocking": { "version": "2.0.0", @@ -1964,6 +2108,32 @@ "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", "integrity": "sha1-y9JDlTzELv/VSLXSI4jtaJ7GOb0=" }, + "string-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", + "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", + "dev": true, + "requires": { + "strip-ansi": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -2070,6 +2240,12 @@ } } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -2336,11 +2512,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "semver": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-3.0.1.tgz", - "integrity": "sha1-cgrAElFaJS+R+w3S6ZpWpw1s8Hg=" } } }, @@ -2356,7 +2527,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", "requires": { "ms": "2.0.0" } diff --git a/oauth/index.js b/oauth/index.js index 786fd32..62166aa 100644 --- a/oauth/index.js +++ b/oauth/index.js @@ -16,7 +16,7 @@ const authHeaderRegex = /Bearer (.+)/; const PRIVATE_JWT_VALUES = ['application_name', 'client_id', 'api_product_list', 'iat', 'exp']; const SUPPORTED_DOUBLE_ASTERIK_PATTERN = "**"; const SUPPORTED_SINGLE_ASTERIK_PATTERN = "*"; -const SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN = "/"; +// const SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN = "/"; const acceptAlg = ['RS256']; @@ -60,7 +60,7 @@ module.exports.init = function(config, logger, stats) { //this flag will enable check against resource paths only productOnly = config.hasOwnProperty('productOnly') ? config.productOnly : false; //if local proxy is set, ignore proxies - if (process.env.EDGEMICRO_LOCAL_PROXY == "1") { + if (process.env.EDGEMICRO_LOCAL_PROXY === "1") { productOnly = true; } //token cache settings @@ -69,6 +69,7 @@ module.exports.init = function(config, logger, stats) { tokenCacheSize = config.hasOwnProperty('tokenCacheSize') ? config.tokenCacheSize : 100; // //support for enabling oauth or api key only + var header = false; if (oauth_only) { if (!req.headers[authHeaderName]) { if (config.allowNoAuthorization) { @@ -78,8 +79,8 @@ module.exports.init = function(config, logger, stats) { return sendError(req, res, next, logger, stats, 'missing_authorization', 'Missing Authorization header'); } } else { - var header = authHeaderRegex.exec(req.headers[authHeaderName]); - if (!header || header.length < 2) { + header = authHeaderRegex.exec(req.headers[authHeaderName]); + if ( !(header) || (header.length < 2) ) { debug('Invalid Authorization Header'); return sendError(req, res, next, logger, stats, 'invalid_request', 'Invalid Authorization header'); } @@ -92,8 +93,9 @@ module.exports.init = function(config, logger, stats) { } //leaving rest of the code same to ensure backward compatibility - if (!req.headers[authHeaderName] || config.allowAPIKeyOnly) { - if (apiKey = req.headers[apiKeyHeaderName]) { + if (!(req.headers[authHeaderName]) || config.allowAPIKeyOnly) { + apiKey = req.headers[apiKeyHeaderName] + if ( apiKey ) { exchangeApiKeyForToken(req, res, next, config, logger, stats, middleware, apiKey); } else if (req.reqUrl && req.reqUrl.query && (apiKey = req.reqUrl.query[apiKeyHeaderName])) { exchangeApiKeyForToken(req, res, next, config, logger, stats, middleware, apiKey); @@ -104,7 +106,8 @@ module.exports.init = function(config, logger, stats) { return sendError(req, res, next, logger, stats, 'missing_authorization', 'Missing Authorization header'); } } else { - var header = authHeaderRegex.exec(req.headers[authHeaderName]); + + header = authHeaderRegex.exec(req.headers[authHeaderName]); if (!config.allowInvalidAuthorization) { if (!header || header.length < 2) { debug('Invalid Authorization Header'); @@ -212,10 +215,10 @@ module.exports.init = function(config, logger, stats) { return sendError(req, res, next, logger, stats, 'invalid_token','token could not be parsed'); } // - if (tokenCache == true) { + if (tokenCache === true) { debug('token caching enabled') map.read(oauthtoken, function(err, tokenvalue) { - if (!err && tokenvalue != undefined && tokenvalue != null && tokenvalue == oauthtoken) { + if (!err && tokenvalue !== undefined && tokenvalue !== null && tokenvalue === oauthtoken) { debug('found token in cache'); isValid = true; if (ejectToken(decodedToken.payloadObj.exp)) { @@ -246,9 +249,9 @@ module.exports.init = function(config, logger, stats) { return sendError(req, res, next, logger, stats, 'invalid_token'); } } else { - if (tokenvalue == null || tokenvalue == undefined) { + if (tokenvalue === null || tokenvalue === undefined) { map.size(function(err, sizevalue) { - if (!err && sizevalue != null && sizevalue < tokenCacheSize) { + if (!err && sizevalue !== null && sizevalue < tokenCacheSize) { map.store(oauthtoken, oauthtoken, decodedToken.payloadObj.exp); } else { debug('too many tokens in cache; ignore storing token'); @@ -312,7 +315,7 @@ module.exports.init = function(config, logger, stats) { return { onrequest: function(req, res, next) { - if (process.env.EDGEMICRO_LOCAL == "1") { + if (process.env.EDGEMICRO_LOCAL === "1") { debug ("MG running in local mode. Skipping OAuth"); next(); } else { @@ -392,7 +395,7 @@ const checkIfAuthorized = module.exports.checkIfAuthorized = function checkIfAut } else { // if(apiproxy.includes(SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN)){ // } - matchesProxyRules = urlPath == apiproxy; + matchesProxyRules = urlPath === apiproxy; } } @@ -415,7 +418,7 @@ function getPEM(decodedToken, keys) { var i = 0; debug('jwk kid ' + decodedToken.headerObj.kid); for (; i < keys.length; i++) { - if (keys.kid == decodedToken.headerObj.kid) { + if (keys.kid === decodedToken.headerObj.kid) { break; } } @@ -430,26 +433,36 @@ function ejectToken(expTimestampInSeconds) { return currentTimestampInSeconds > expTimestampInSeconds + gracePeriod } -function sendError(req, res, next, logger, stats, code, message) { - - switch (code) { - case 'invalid_request': +function setResponseCode(res,code) { + switch ( code ) { + case 'invalid_request': { res.statusCode = 400; break; - case 'access_denied': + } + case 'access_denied':{ res.statusCode = 403; break; + } case 'invalid_token': case 'missing_authorization': - case 'invalid_authorization': + case 'invalid_authorization': { res.statusCode = 401; break; - case 'gateway_timeout': + } + case 'gateway_timeout': { res.statusCode = 504; break; - default: + } + default: { res.statusCode = 500; + break; + } } +} + +function sendError(req, res, next, logger, stats, code, message) { + + setResponseCode(res,code); var response = { error: code, diff --git a/oauthv2/index.js b/oauthv2/index.js index 197efe0..0d36b65 100644 --- a/oauthv2/index.js +++ b/oauthv2/index.js @@ -3,19 +3,17 @@ var debug = require('debug')('plugin:oauthv2'); var url = require('url'); var rs = require('jsrsasign'); -var fs = require('fs'); -var path = require('path'); const memoredpath = '../third_party/memored/index'; var map = require(memoredpath); var JWS = rs.jws.JWS; -var requestLib = require('request'); +//var requestLib = require('request'); var _ = require('lodash'); const authHeaderRegex = /Bearer (.+)/; const PRIVATE_JWT_VALUES = ['application_name', 'client_id', 'api_product_list', 'iat', 'exp']; const SUPPORTED_DOUBLE_ASTERIK_PATTERN = "**"; const SUPPORTED_SINGLE_ASTERIK_PATTERN = "*"; -const SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN = "/"; +//const SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN = "/"; const acceptAlg = ['RS256']; @@ -33,7 +31,7 @@ var tokenCacheSize = 100; module.exports.init = function(config, logger, stats) { - var request = config.request ? requestLib.defaults(config.request) : requestLib; + //var request = config.request ? requestLib.defaults(config.request) : requestLib; var keys = config.jwk_keys ? JSON.parse(config.jwk_keys) : null; var middleware = function(req, res, next) { @@ -46,7 +44,7 @@ module.exports.init = function(config, logger, stats) { //this flag will enable check against resource paths only productOnly = config.hasOwnProperty('productOnly') ? config.productOnly : false; //if local proxy is set, ignore proxies - if (process.env.EDGEMICRO_LOCAL_PROXY == "1") { + if (process.env.EDGEMICRO_LOCAL_PROXY === "1") { productOnly = true; } //token cache settings @@ -54,6 +52,7 @@ module.exports.init = function(config, logger, stats) { //max number of tokens in the cache tokenCacheSize = config.hasOwnProperty('tokenCacheSize') ? config.tokenCacheSize : 100; // + var header = false; if (!req.headers[authHeaderName]) { if (config.allowNoAuthorization) { return next(); @@ -62,8 +61,8 @@ module.exports.init = function(config, logger, stats) { return sendError(req, res, next, logger, stats, 'missing_authorization', 'Missing Authorization header'); } } else { - var header = authHeaderRegex.exec(req.headers[authHeaderName]); - if (!header || header.length < 2) { + header = authHeaderRegex.exec(req.headers[authHeaderName]); + if (!(header) || (header.length < 2) ) { debug('Invalid Authorization Header'); return sendError(req, res, next, logger, stats, 'invalid_request', 'Invalid Authorization header'); } @@ -98,10 +97,10 @@ module.exports.init = function(config, logger, stats) { } } - if (tokenCache == true) { + if (tokenCache === true) { debug('token caching enabled') map.read(oauthtoken, function(err, tokenvalue) { - if (!err && tokenvalue != undefined && tokenvalue != null && tokenvalue == oauthtoken) { + if ( !err && (tokenvalue !== undefined) && (tokenvalue !== null) && (tokenvalue === oauthtoken) ) { debug('found token in cache'); isValid = true; if (ejectToken(decodedToken.payloadObj.exp)) { @@ -132,9 +131,9 @@ module.exports.init = function(config, logger, stats) { return sendError(req, res, next, logger, stats, 'invalid_token'); } } else { - if (tokenvalue == null || tokenvalue == undefined) { + if (tokenvalue === null || tokenvalue === undefined) { map.size(function(err, sizevalue) { - if (!err && sizevalue != null && sizevalue < tokenCacheSize) { + if (!err && sizevalue !== null && sizevalue < tokenCacheSize) { map.store(oauthtoken, oauthtoken, decodedToken.payloadObj.exp); } else { debug('too many tokens in cache; ignore storing token'); @@ -184,7 +183,7 @@ module.exports.init = function(config, logger, stats) { return { onrequest: function(req, res, next) { - if (process.env.EDGEMICRO_LOCAL == "1") { + if (process.env.EDGEMICRO_LOCAL === "1") { debug ("MG running in local mode. Skipping OAuth"); next(); } else { @@ -259,7 +258,7 @@ const checkIfAuthorized = module.exports.checkIfAuthorized = function checkIfAut } else { // if(apiproxy.includes(SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN)){ // } - matchesProxyRules = urlPath == apiproxy; + matchesProxyRules = urlPath === apiproxy; } } @@ -282,7 +281,7 @@ function getPEM(decodedToken, keys) { var i = 0; debug('jwk kid ' + decodedToken.headerObj.kid); for (; i < keys.length; i++) { - if (keys.kid == decodedToken.headerObj.kid) { + if (keys.kid === decodedToken.headerObj.kid) { break; } } @@ -297,26 +296,36 @@ function ejectToken(expTimestampInSeconds) { return currentTimestampInSeconds > expTimestampInSeconds + gracePeriod } -function sendError(req, res, next, logger, stats, code, message) { - - switch (code) { - case 'invalid_request': +function setResponseCode(res,code) { + switch ( code ) { + case 'invalid_request': { res.statusCode = 400; break; - case 'access_denied': + } + case 'access_denied':{ res.statusCode = 403; break; + } case 'invalid_token': case 'missing_authorization': - case 'invalid_authorization': + case 'invalid_authorization': { res.statusCode = 401; break; - case 'gateway_timeout': + } + case 'gateway_timeout': { res.statusCode = 504; break; - default: + } + default: { res.statusCode = 500; + break; + } } +} + +function sendError(req, res, next, logger, stats, code, message) { + + setResponseCode(res,code); var response = { error: code, diff --git a/package.json b/package.json index 1ed03f3..ef8ba74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "microgateway-plugins", - "version": "3.0.0", + "version": "3.0.1", "description": "Plugins for Apige Edge Microgateway", "license": "Apache-2.0", "main": "index.js", @@ -36,6 +36,7 @@ "chai": "^2.3.0", "config": "^1.13.0", "js-yaml": "^3.4.2", + "jshint-stylish": "^2.2.1", "jsonwebtoken": "^5.0.1", "mocha": "^5.2.0", "nyc": "^14.1.1", diff --git a/quota-memory/index.js b/quota-memory/index.js index 3df8daa..015aaed 100644 --- a/quota-memory/index.js +++ b/quota-memory/index.js @@ -11,7 +11,7 @@ var debug = require('debug')('gateway:quota'); -module.exports.init = function(config, logger, stats) { +module.exports.init = function(config /*, logger, stats */) { var quotas = {}; // productName -> connectMiddleware var options = { @@ -42,7 +42,8 @@ module.exports.init = function(config, logger, stats) { function(productName, cb) { var connectMiddleware = quotas[productName]; debug('applying quota for', productName); - connectMiddleware ? connectMiddleware(req, res, cb) : cb(); + if ( connectMiddleware ){ connectMiddleware(req, res, cb) } else cb(); + }, function(err) { next(err); diff --git a/quota/index.js b/quota/index.js index 22e3b9c..4d771de 100644 --- a/quota/index.js +++ b/quota/index.js @@ -10,7 +10,7 @@ var debug = require('debug')('gateway:quota'); var url = require('url'); -module.exports.init = function(config, logger, stats) { +module.exports.init = function(config /*, logger, stats */) { const { product_to_proxy, proxies } = config; const prodsObj = {}; @@ -23,7 +23,7 @@ module.exports.init = function(config, logger, stats) { var quotaManagers = {} - if ( product_to_proxy == undefined || proxies== undefined ) { + if ( (product_to_proxy === undefined) || (proxies === undefined) ) { // debug("quota plugin did not recieve valid produc-proxy map or list of proxies") return(undefined) @@ -37,9 +37,9 @@ module.exports.init = function(config, logger, stats) { return; } - if(product.timeUnit === 'month') { - product.timeUnit === '30days'; - }; + if ( product.timeUnit === 'month' ) { + //product.timeUnit = '30days'; // this is broken - volos does not consider 30days as an option, but tries to process it anyway. + } const prodProxiesArr = product_to_proxy[productName]; @@ -104,7 +104,7 @@ module.exports.init = function(config, logger, stats) { function(productName, cb) { var connectMiddleware = quotas[productName]; debug('applying quota for', productName); - connectMiddleware ? connectMiddleware(req, res, cb) : cb(); + if ( connectMiddleware ){ connectMiddleware(req, res, cb) } else cb(); }, function(err) { next(err); diff --git a/spikearrest/index.js b/spikearrest/index.js index 16dee85..58fe8a1 100644 --- a/spikearrest/index.js +++ b/spikearrest/index.js @@ -1,8 +1,8 @@ 'use strict'; -var debug = require('debug')('plugin:spikearrest'); +//var debug = require('debug')('plugin:spikearrest'); -module.exports.init = function(config, logger, stats) { +module.exports.init = function(config /*, logger, stats */) { var spikearrest = require('volos-spikearrest-memory').create(config); var middleware = spikearrest.connectMiddleware().apply(); diff --git a/test/quota-test.js b/test/quota-test.js index da3ff5f..e237643 100644 --- a/test/quota-test.js +++ b/test/quota-test.js @@ -145,9 +145,50 @@ describe('quota plugin', function() { assert.ok(plugin.onrequest); done(); }); + + it('will throw on bad time unit',(done) => { + try { + quota.init.apply(null, [exampleBogusConfig_timeUnit, logger, stats]); + } catch(e) { + assert(true); + } + done(); + }) + it('will throw on no URI',(done) => { + try { + quota.init.apply(null, [exampleBogusConfig_NoURI_NOKEY, logger, stats]); + } catch(e) { + assert(true); + } + done(); + }) + + + it('will throw on no KEY',(done) => { + try { + exampleBogusConfig_NoURI_NOKEY.EdgeMicroTestProduct.uri = exampleConfig.EdgeMicroTestProduct.uri + quota.init.apply(null, [exampleBogusConfig_NoURI_NOKEY, logger, stats]); + } catch(e) { + assert(true); + } + done(); + }) + + it('defers to a local handler',(done) => { + // + var pars = [].concat(generic_req_params) + pars[2] = () => { + delete process.env.EDGEMICRO_LOCAL; + done(); + } + process.env.EDGEMICRO_LOCAL = "This is a test" + plugin.onrequest.apply(null, pars); + // + }) + it('will throw on bad time unit',(done) => { try { quota.init.apply(null, [exampleBogusConfig_timeUnit, logger, stats]); diff --git a/third_party/memored/index.js b/third_party/memored/index.js index 0ccb2e4..09690fb 100644 --- a/third_party/memored/index.js +++ b/third_party/memored/index.js @@ -55,7 +55,7 @@ function _findWorkerByPid(workerPid) { worker; for (; i < len; i++) { - if (cluster.workers[workerIds[i]].process.pid == workerPid) { + if (cluster.workers[workerIds[i]].process.pid === workerPid) { worker = cluster.workers[workerIds[i]]; break; } @@ -78,7 +78,7 @@ function _getResultParamsValues(paramsObj) { function _sendMessageToWorker(message) { var worker = _findWorkerByPid(message.workerPid); //check if the process is up - if (worker && worker.isDead() == false) worker.send(message); + if (worker && worker.isDead() === false) worker.send(message); } function _sendMessageToMaster(message) { diff --git a/transform-uppercase/index.js b/transform-uppercase/index.js index d645d73..7e5514a 100644 --- a/transform-uppercase/index.js +++ b/transform-uppercase/index.js @@ -7,7 +7,7 @@ * plugin, which will accumulate chunks and deliver them concatenated as one data * Buffer to the onend handler. */ -module.exports.init = function(config, logger, stats) { +module.exports.init = function(/* config, logger, stats */) { // perform content transformation here // the result of the transformation must be another Buffer