UNPKG

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