UNPKG

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