UNPKG

microgateway-plugins

Version:
545 lines (427 loc) 16.8 kB
'use strict'; var debug = require('debug')('plugin:basicAuth'); var url = require('url'); // var rs = require('jsrsasign'); var JWS = rs.jws.JWS; // const emgCache = '../emgCache'; var map = require(emgCache); function _enableCache() { return (require(emgCache)); } map.setup({ purgeInterval: 10000 }); const CONSOLE_LOG_TAG_COMP = 'microgateway-plugins basicAuth'; // // 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) { // TODO: convert to logger.eventLog this.logger.consoleLog('warn',{component: CONSOLE_LOG_TAG_COMP}, 'error parsing jwt: '); } return(isValid) } tryValidationBipass(req, res) { if ( this.allowInvalidAuthorization ) { // TODO: convert to logger.eventLog this.logger.consoleLog('warn', {component: CONSOLE_LOG_TAG_COMP}, '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); req.token = decodedToken.payloadObj; } catch (err) { if ( this.allowInvalidAuthorization ) { // TODO: convert to logger.eventLog this.logger.consoleLog('warn', {component: CONSOLE_LOG_TAG_COMP},'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) { req.token = decodedToken; // if ( this.checkIfAuthorized(this.config, req.reqUrl.path, res.proxy, decodedToken) ) { // var authClaims = _objectWithoutProperties(decodedToken, PRIVATE_JWT_VALUES); req.headers['x-authorization-claims'] = Buffer.from(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 } let result = matchesProxyRules; if (!productOnly) result = matchesProxyRules && validProxyNames.indexOf(proxy.name) >= 0; if(result) req.api_product = product; return result; }); } // 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 }; const err = Error(message); debug('auth failure', res.statusCode, code, message ? message : '', req.headers, req.method, req.url); if ( logger && logger.error && (typeof logger.error === 'function') ) { logger.eventLog({level:'error', req: req, res: res, err:err, component:'basicAuth' }, 'oauth'); } //opentracing if (process.env.EDGEMICRO_OPENTRACE) { try { if (logger.traceHelper) { logger.traceHelper.setChildErrorSpan('oauth', req.headers); } } catch (err) { } } // if ( !res.finished ) { try { res.setHeader('content-type', 'application/json'); } catch (e) { // TODO: convert to logger.eventLog logger.consoleLog('warn',{component: CONSOLE_LOG_TAG_COMP}, "oath response object lacks setHeader"); } } try { res.end(JSON.stringify(response)); } catch (e) { // TODO: convert to logger.eventLog logger.consoleLog('warn',{component: CONSOLE_LOG_TAG_COMP}, "oath response object is not supplied by runtime"); } try { stats.incrementStatusCount(res.statusCode); } catch (e) { // TODO: convert to logger.eventLog logger.consoleLog('warn',{component: CONSOLE_LOG_TAG_COMP}, "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 }