UNPKG

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