1 | "use strict";
|
2 |
|
3 | var debug = require("debug")("plugin:apikeys");
|
4 | var url = require("url");
|
5 | var rs = require("jsrsasign");
|
6 | var fs = require("fs");
|
7 | var path = require("path");
|
8 | const memoredpath = '../third_party/memored/index';
|
9 | var cache = require(memoredpath);
|
10 | var JWS = rs.jws.JWS;
|
11 | var requestLib = require("request");
|
12 | var _ = require("lodash");
|
13 |
|
14 | const PRIVATE_JWT_VALUES = ["application_name", "client_id", "api_product_list", "iat", "exp"];
|
15 | const SUPPORTED_DOUBLE_ASTERIK_PATTERN = "**";
|
16 | const SUPPORTED_SINGLE_ASTERIK_PATTERN = "*";
|
17 |
|
18 |
|
19 | const acceptAlg = ["RS256"];
|
20 |
|
21 | var acceptField = {};
|
22 | acceptField.alg = acceptAlg;
|
23 |
|
24 | var productOnly;
|
25 | var cacheKey = false;
|
26 |
|
27 | const LOG_TAG_COMP = 'apikeys';
|
28 | const CONSOLE_LOG_TAG_COMP = 'microgateway-plugins apikeys';
|
29 |
|
30 | module.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 |
|
39 | var keepApiKey = config.hasOwnProperty('keep-api-key') ? config['keep-api-key'] : false;
|
40 |
|
41 | cacheKey = config.hasOwnProperty("cacheKey") ? config.cacheKey : false;
|
42 |
|
43 | var gracePeriod = config.hasOwnProperty("gracePeriod") ? config.gracePeriod : 0;
|
44 | acceptField.gracePeriod = gracePeriod;
|
45 |
|
46 | var apiKey;
|
47 |
|
48 | productOnly = config.hasOwnProperty("productOnly") ? config.productOnly : false;
|
49 |
|
50 | if (process.env.EDGEMICRO_LOCAL_PROXY === "1") {
|
51 | productOnly = true;
|
52 | }
|
53 |
|
54 |
|
55 | apiKey = req.headers[apiKeyHeaderName]
|
56 | if ( apiKey ) {
|
57 | if (!keepApiKey) {
|
58 | delete(req.headers[apiKeyHeaderName]);
|
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)) {
|
76 | cache.read(apiKey, function(err, value) {
|
77 | if (value) {
|
78 | if (Date.now() / 1000 < value.exp) {
|
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)) {
|
220 |
|
221 | decodedToken.exp = decodedToken.exp || +(((Date.now() / 1000) + 1800).toFixed(0));
|
222 |
|
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 |
|
238 |
|
239 | const 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 |
|
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 |
|
291 |
|
292 | matchesProxyRules = urlPath === apiproxy;
|
293 |
|
294 | }
|
295 | }
|
296 | })
|
297 |
|
298 | } else {
|
299 | matchesProxyRules = true
|
300 | }
|
301 |
|
302 | debug("matches proxy rules: " + matchesProxyRules);
|
303 |
|
304 | if (!productOnly)
|
305 | return matchesProxyRules && validProxyNames.indexOf(proxy.name) >= 0;
|
306 | else
|
307 | return matchesProxyRules;
|
308 | });
|
309 | }
|
310 |
|
311 | function 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 |
|
323 | function 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 |
|
351 | function 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 |
|
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 | }
|