UNPKG

20.8 kBJavaScriptView Raw
1'use strict';
2
3var debug = require('debug')('plugin:oauth');
4var url = require('url');
5var rs = require('jsrsasign');
6var fs = require('fs');
7var path = require('path');
8const memoredpath = '../third_party/memored/index';
9var cache = require(memoredpath);
10var map = require(memoredpath);
11var JWS = rs.jws.JWS;
12var requestLib = require('request');
13var _ = require('lodash');
14
15const authHeaderRegex = /Bearer (.+)/;
16const PRIVATE_JWT_VALUES = ['application_name', 'client_id', 'api_product_list', 'iat', 'exp'];
17const SUPPORTED_DOUBLE_ASTERIK_PATTERN = "**";
18const SUPPORTED_SINGLE_ASTERIK_PATTERN = "*";
19// const SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN = "/";
20
21const LOG_TAG_COMP = 'oauth';
22
23const acceptAlg = ['RS256'];
24
25var acceptField = {};
26acceptField.alg = acceptAlg;
27
28var productOnly;
29var cacheKey = false;
30//setup cache for oauth tokens
31var tokenCache = false;
32map.setup({
33 purgeInterval: 10000
34});
35
36var tokenCacheSize = 100;
37
38let oauthConfigObj = null;
39
40module.exports.init = function(config, logger, stats) {
41
42 if ( config === undefined || !config ) return(undefined);
43
44 oauthConfigObj = config;
45
46 var request = config.request ? requestLib.defaults(config.request) : requestLib;
47 var keys = config.jwk_keys ? JSON.parse(config.jwk_keys) : null;
48
49 var middleware = function(req, res, next) {
50
51 if ( !req || !res ) return(-1); // need to check bad args
52 if ( !req.headers ) return(-1); // or throw -- means callers are bad
53
54 var authHeaderName = config.hasOwnProperty('authorization-header') ? config['authorization-header'] : 'authorization';
55 var apiKeyHeaderName = config.hasOwnProperty('api-key-header') ? config['api-key-header'] : 'x-api-key';
56 var keepAuthHeader = config.hasOwnProperty('keep-authorization-header') ? config['keep-authorization-header'] : false;
57 cacheKey = config.hasOwnProperty('cacheKey') ? config.cacheKey : false;
58 //set grace period
59 var gracePeriod = config.hasOwnProperty('gracePeriod') ? config.gracePeriod : 0;
60 acceptField.gracePeriod = gracePeriod;
61 //support for enabling oauth or api key only
62 var oauth_only = config.hasOwnProperty('allowOAuthOnly') ? config.allowOAuthOnly : false;
63 var apikey_only = config.hasOwnProperty('allowAPIKeyOnly') ? config.allowAPIKeyOnly : false;
64 //
65 var apiKey;
66 //this flag will enable check against resource paths only
67 productOnly = config.hasOwnProperty('productOnly') ? config.productOnly : false;
68 //if local proxy is set, ignore proxies
69 if (process.env.EDGEMICRO_LOCAL_PROXY === "1") {
70 productOnly = true;
71 }
72 //token cache settings
73 tokenCache = config.hasOwnProperty('tokenCache') ? config.tokenCache : false;
74 //max number of tokens in the cache
75 tokenCacheSize = config.hasOwnProperty('tokenCacheSize') ? config.tokenCacheSize : 100;
76 //
77 //support for enabling oauth or api key only
78 var header = false;
79 if (oauth_only) {
80 if (!req.headers[authHeaderName]) {
81 if (config.allowNoAuthorization) {
82 return next();
83 } else {
84 debug('missing_authorization');
85 return sendError(req, res, next, logger, stats, 'missing_authorization', 'Missing Authorization header');
86 }
87 } else {
88 header = authHeaderRegex.exec(req.headers[authHeaderName]);
89 if ( !(header) || (header.length < 2) ) {
90 debug('Invalid Authorization Header');
91 return sendError(req, res, next, logger, stats, 'invalid_request', 'Invalid Authorization header');
92 }
93 }
94 } else if (apikey_only) {
95 if (!req.headers[apiKeyHeaderName]) {
96 debug('missing api key');
97 return sendError(req, res, next, logger, stats, 'invalid_authorization', 'Missing API Key header');
98 }
99 }
100
101 //leaving rest of the code same to ensure backward compatibility
102 if (!(req.headers[authHeaderName]) || config.allowAPIKeyOnly) {
103 apiKey = req.headers[apiKeyHeaderName]
104 if ( apiKey ) {
105 exchangeApiKeyForToken(req, res, next, config, logger, stats, middleware, apiKey);
106 } else if (req.reqUrl && req.reqUrl.query && (apiKey = req.reqUrl.query[apiKeyHeaderName])) {
107 exchangeApiKeyForToken(req, res, next, config, logger, stats, middleware, apiKey);
108 } else if (config.allowNoAuthorization) {
109 return next();
110 } else {
111 debug('missing_authorization');
112 return sendError(req, res, next, logger, stats, 'missing_authorization', 'Missing Authorization header');
113 }
114 } else {
115
116 header = authHeaderRegex.exec(req.headers[authHeaderName]);
117 if (!config.allowInvalidAuthorization) {
118 if (!header || header.length < 2) {
119 debug('Invalid Authorization Header');
120 return sendError(req, res, next, logger, stats, 'invalid_request', 'Invalid Authorization header');
121 }
122 }
123
124 if (!keepAuthHeader) {
125 delete(req.headers[authHeaderName]); // don't pass this header to target
126 }
127
128 var token = '';
129 if (header) {
130 token = header[1];
131 }
132 verify(token, config, logger, stats, middleware, req, res, next);
133 }
134 }
135
136 var exchangeApiKeyForToken = function(req, res, next, config, logger, stats, middleware, apiKey) {
137 var cacheControl = req.headers['cache-control'] || 'no-cache';
138 if (cacheKey || (!cacheControl || (cacheControl && cacheControl.indexOf('no-cache') < 0))) { // caching is allowed
139 cache.read(apiKey, function(err, value) {
140 if (value) {
141 if (Date.now() / 1000 < value.exp) { // not expired yet (token expiration is in seconds)
142 debug('api key cache hit', apiKey);
143 return authorize(req, res, next, logger, stats, value);
144 } else {
145 cache.remove(apiKey);
146 debug('api key cache expired', apiKey);
147 requestApiKeyJWT(req, res, next, config, logger, stats, middleware, apiKey);
148 }
149 } else {
150 debug('api key cache miss', apiKey);
151 requestApiKeyJWT(req, res, next, config, logger, stats, middleware, apiKey);
152 }
153 });
154 } else {
155 requestApiKeyJWT(req, res, next, config, logger, stats, middleware, apiKey);
156 }
157
158 }
159
160 function requestApiKeyJWT(req, res, next, config, logger, stats, middleware, apiKey) {
161
162 if (!config.verify_api_key_url) return sendError(req, res, next, logger, stats, 'invalid_request', 'API Key Verification URL not configured');
163
164 var api_key_options = {
165 url: config.verify_api_key_url,
166 method: 'POST',
167 json: {
168 'apiKey': apiKey
169 },
170 headers: {
171 'x-dna-api-key': apiKey
172 }
173 };
174
175 if (config.agentOptions) {
176 if (config.agentOptions.requestCert) {
177 api_key_options.requestCert = true;
178 if (config.agentOptions.cert && config.agentOptions.key) {
179 api_key_options.key = fs.readFileSync(path.resolve(config.agentOptions.key), 'utf8');
180 api_key_options.cert = fs.readFileSync(path.resolve(config.agentOptions.cert), 'utf8');
181 if (config.agentOptions.ca) api_key_options.ca = fs.readFileSync(path.resolve(config.agentOptions.ca), 'utf8');
182 } else if (config.agentOptions.pfx) {
183 api_key_options.pfx = fs.readFileSync(path.resolve(config.agentOptions.pfx));
184 }
185 if (config.agentOptions.rejectUnauthorized) {
186 api_key_options.rejectUnauthorized = true;
187 }
188 if (config.agentOptions.secureProtocol) {
189 api_key_options.secureProtocol = true;
190 }
191 if (config.agentOptions.ciphers) {
192 api_key_options.ciphers = config.agentOptions.ciphers;
193 }
194 if (config.agentOptions.passphrase) api_key_options.passphrase = config.agentOptions.passphrase;
195 }
196 }
197 //debug(api_key_options);
198 request(api_key_options, function(err, response, body) {
199 if (err) {
200 debug('verify apikey gateway timeout');
201 return sendError(req, res, next, logger, stats, 'gateway_timeout', err.message);
202 }
203 if (response.statusCode !== 200) {
204 debug('verify apikey failure',response.statusCode, response.statusMessage, body);
205 return sendError(req, res, next, logger, stats, 'access_denied', response.statusMessage,response);
206 }
207 verify(body, config, logger, stats, middleware, req, res, next, apiKey);
208 });
209 }
210
211 var verify = function(token, config, logger, stats, middleware, req, res, next, apiKey) {
212
213 var isValid = false;
214 var oauthtoken = token && token.token ? token.token : token;
215 var decodedToken = null;
216 //
217 try {
218 decodedToken = JWS.parse(oauthtoken);
219 } catch(e) {
220 // 'invalid_token'
221 return sendError(req, res, next, logger, stats, 'invalid_token','token could not be parsed');
222 }
223 //
224 if (tokenCache === true) {
225 debug('token caching enabled')
226 map.read(oauthtoken, function(err, tokenvalue) {
227 if (!err && tokenvalue !== undefined && tokenvalue !== null && tokenvalue === oauthtoken) {
228 debug('found token in cache');
229 isValid = true;
230 if (ejectToken(decodedToken.payloadObj.exp)) {
231 debug('ejecting token from cache');
232 map.remove(oauthtoken);
233 }
234 } else {
235 debug('token not found in cache');
236 try {
237 if (keys) {
238 debug('using jwk');
239 var pem = getPEM(decodedToken, keys);
240 isValid = JWS.verifyJWT(oauthtoken, pem, acceptField);
241 } else {
242 debug('validating jwt');
243 isValid = JWS.verifyJWT(oauthtoken, config.public_key, acceptField);
244 }
245 } catch (error) {
246 logger.eventLog({level:'warn', req: req, res: res, err:err, component:LOG_TAG_COMP }, 'error parsing jwt: ' + oauthtoken);
247 }
248 }
249 if (!isValid) {
250 if (config.allowInvalidAuthorization) {
251 logger.eventLog({level:'warn', req: req, res: res, err:err, component:LOG_TAG_COMP }, 'ignoring err in verify');
252 return next();
253 } else {
254 debug('invalid token');
255 return sendError(req, res, next, logger, stats, 'invalid_token', 'invalid_token');
256 }
257 } else {
258 if (tokenvalue === null || tokenvalue === undefined) {
259 map.size(function(err, sizevalue) {
260 if (!err && sizevalue !== null && sizevalue < tokenCacheSize) {
261 map.store(oauthtoken, oauthtoken, decodedToken.payloadObj.exp);
262 } else {
263 debug('too many tokens in cache; ignore storing token');
264 }
265 });
266 }
267 authorize(req, res, next, logger, stats, decodedToken.payloadObj, apiKey);
268 }
269 });
270 } else {
271 try {
272 if (keys) {
273 debug('using jwk');
274 var pem = getPEM(decodedToken, keys);
275 isValid = JWS.verifyJWT(oauthtoken, pem, acceptField);
276 } else {
277 debug('validating jwt');
278 isValid = JWS.verifyJWT(oauthtoken, config.public_key, acceptField);
279 }
280 } catch (error) {
281 logger.eventLog({level:'warn', req: req, res: res, err:error, component:LOG_TAG_COMP }, 'error parsing jwt: ' + oauthtoken);
282 }
283 if (!isValid) {
284 if (config.allowInvalidAuthorization) {
285 logger.eventLog({level:'warn', req: req, res: res, err:null, component:LOG_TAG_COMP }, 'ignoring err');
286 return next();
287 } else {
288 debug('invalid token');
289 return sendError(req, res, next, logger, stats, 'invalid_token', 'invalid_token');
290 }
291 } else {
292 authorize(req, res, next, logger, stats, decodedToken.payloadObj, apiKey);
293 }
294 }
295 };
296
297 function authorize(req, res, next, logger, stats, decodedToken, apiKey) {
298 if (checkIfAuthorized(config, req.reqUrl.path, res.proxy, decodedToken)) {
299 req.token = decodedToken;
300
301 var authClaims = _.omit(decodedToken, PRIVATE_JWT_VALUES);
302 req.headers['x-authorization-claims'] = new Buffer(JSON.stringify(authClaims)).toString('base64');
303
304 if (apiKey) {
305 var cacheControl = req.headers['cache-control'] || 'no-cache';
306 if (cacheKey || (!cacheControl || (cacheControl && cacheControl.indexOf('no-cache') < 0))) { // caching is allowed
307 // default to now (in seconds) + 30m if not set
308 decodedToken.exp = decodedToken.exp || +(((Date.now() / 1000) + 1800).toFixed(0));
309 //apiKeyCache[apiKey] = decodedToken;
310 cache.store(apiKey, decodedToken,decodedToken.exp);
311 debug('api key cache store', apiKey);
312 } else {
313 debug('api key cache skip', apiKey);
314 }
315 }
316 next();
317 } else {
318 return sendError(req, res, next, logger, stats, 'access_denied','access_denied');
319 }
320 }
321
322 return {
323 onrequest: function(req, res, next) {
324 if (process.env.EDGEMICRO_LOCAL === "1") {
325 debug ("MG running in local mode. Skipping OAuth");
326 next();
327 } else {
328 middleware(req, res, next);
329 }
330 },
331
332 shutdown() {
333 // tests are needing shutdowns to remove services that keep programs running, etc.
334 },
335
336 testing: {
337 ejectToken
338 }
339 };
340
341} // end of init
342
343
344
345
346
347
348
349// from the product name(s) on the token, find the corresponding proxy
350// then check if that proxy is one of the authorized proxies in bootstrap
351const checkIfAuthorized = module.exports.checkIfAuthorized = function checkIfAuthorized(config, urlPath, proxy, decodedToken) {
352
353 var parsedUrl = url.parse(urlPath);
354 //
355 debug('product only: ' + productOnly);
356 //
357
358 if (!decodedToken.api_product_list) {
359 debug('no api product list');
360 return false;
361 }
362
363 return decodedToken.api_product_list.some(function(product) {
364
365 const validProxyNames = config.product_to_proxy[product];
366
367 if (!productOnly) {
368 if (!validProxyNames) {
369 debug('no proxies found for product');
370 return false;
371 }
372 }
373
374
375 const apiproxies = config.product_to_api_resource[product];
376
377 var matchesProxyRules = false;
378 if (apiproxies && apiproxies.length) {
379 apiproxies.forEach(function(tempApiProxy) {
380 if (matchesProxyRules) {
381 //found one
382 debug('found matching proxy rule');
383 return;
384 }
385
386 urlPath = parsedUrl.pathname;
387 const apiproxy = tempApiProxy.includes(proxy.base_path) ?
388 tempApiProxy :
389 proxy.base_path + (tempApiProxy.startsWith("/") ? "" : "/") + tempApiProxy
390 if (apiproxy.endsWith("/") && !urlPath.endsWith("/")) {
391 urlPath = urlPath + "/";
392 }
393
394 if (apiproxy.includes(SUPPORTED_DOUBLE_ASTERIK_PATTERN)) {
395 const regex = apiproxy.replace(/\*\*/gi, ".*")
396 matchesProxyRules = urlPath.match(regex)
397 } else {
398 if (apiproxy.includes(SUPPORTED_SINGLE_ASTERIK_PATTERN)) {
399 const regex = apiproxy.replace(/\*/gi, "[^/]+");
400 matchesProxyRules = urlPath.match(regex)
401 } else {
402 // if(apiproxy.includes(SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN)){
403 // }
404 matchesProxyRules = urlPath === apiproxy;
405
406 }
407 }
408 })
409
410 } else {
411 matchesProxyRules = true
412 }
413
414 debug("matches proxy rules: " + matchesProxyRules);
415 //add pattern matching here
416 if (!productOnly)
417 return matchesProxyRules && validProxyNames.indexOf(proxy.name) >= 0;
418 else
419 return matchesProxyRules;
420 });
421}
422
423function getPEM(decodedToken, keys) {
424 var i = 0;
425 debug('jwk kid ' + decodedToken.headerObj.kid);
426 for (; i < keys.length; i++) {
427 if (keys.kid === decodedToken.headerObj.kid) {
428 break;
429 }
430 }
431 var publickey = rs.KEYUTIL.getKey(keys.keys[i]);
432 return rs.KEYUTIL.getPEM(publickey);
433}
434
435// this should be in a separate module. This code is being copied from instance to instance.
436function ejectToken(expTimestampInSeconds) {
437 var currentTimestampInSeconds = new Date().getTime() / 1000;
438 var gracePeriod = parseInt(acceptField.gracePeriod)
439 return currentTimestampInSeconds > expTimestampInSeconds + gracePeriod
440}
441
442function setResponseCode(res,code) {
443 switch ( code ) {
444 case 'invalid_request': {
445 res.statusCode = 400;
446 break;
447 }
448 case 'access_denied':{
449 res.statusCode = 403;
450 break;
451 }
452 case 'invalid_token':
453 case 'missing_authorization':
454 case 'invalid_authorization': {
455 res.statusCode = 401;
456 break;
457 }
458 case 'gateway_timeout': {
459 res.statusCode = 504;
460 break;
461 }
462 default: {
463 res.statusCode = 500;
464 break;
465 }
466 }
467}
468
469function sendError(req, res, next, logger, stats, code, message, upstreamResp) {
470
471 if ( upstreamResp && oauthConfigObj.hasOwnProperty('useUpstreamResponse') && oauthConfigObj.useUpstreamResponse === true ) {
472 res.statusCode = upstreamResp.statusCode;
473 res.statusMessage = upstreamResp.statusMessage;
474 code = 'upstream_error';
475 } else {
476 setResponseCode(res,code);
477 }
478
479 var response = {
480 error: code,
481 error_description: message
482 };
483 const err = Error(message);
484 debug('auth failure', res.statusCode, code, message ? message : '', req.headers, req.method, req.url);
485 logger.eventLog({ level:'error', req: req, res: res, err: err,component:'oauth'}, message);
486
487 //opentracing
488 if (process.env.EDGEMICRO_OPENTRACE) {
489 try {
490 const traceHelper = require('../microgateway-core/lib/trace-helper');
491 traceHelper.setChildErrorSpan('oauth', req.headers);
492 } catch (err) {}
493 }
494 //
495
496 if ( !res.finished ) {
497 try {
498 res.setHeader('content-type', 'application/json');
499 } catch (e) {
500 logger.eventLog({level:'warn', req: req, res: res, err:null, component:LOG_TAG_COMP }, "ouath response object lacks setHeader");
501 }
502 }
503
504 try {
505 res.end(JSON.stringify(response));
506 } catch (e) {
507 logger.eventLog({level:'warn', req: req, res: res, err:null, component:LOG_TAG_COMP }, "ouath response object is not supplied by runtime");
508 }
509
510 try {
511 stats.incrementStatusCount(res.statusCode);
512 } catch (e) {
513 logger.eventLog({level:'warn', req: req, res: res, err:null, component:LOG_TAG_COMP }, "ouath stats object is not supplied by runtime");
514 }
515
516 next(code, message);
517 return code;
518}