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 | var cache = require("memored");
|
9 | var JWS = rs.jws.JWS;
|
10 | var requestLib = require("request");
|
11 | var _ = require("lodash");
|
12 |
|
13 | const PRIVATE_JWT_VALUES = ["application_name", "client_id", "api_product_list", "iat", "exp"];
|
14 | const SUPPORTED_DOUBLE_ASTERIK_PATTERN = "**";
|
15 | const SUPPORTED_SINGLE_ASTERIK_PATTERN = "*";
|
16 | const SUPPORTED_SINGLE_FORWARD_SLASH_PATTERN = "/";
|
17 |
|
18 | const acceptAlg = ["RS256"];
|
19 |
|
20 | var acceptField = {};
|
21 | acceptField.alg = acceptAlg;
|
22 |
|
23 | var productOnly;
|
24 | var cacheKey = false;
|
25 |
|
26 | module.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 |
|
35 | var keepApiKey = config.hasOwnProperty('keep-api-key') ? config['keep-api-key'] : false;
|
36 |
|
37 | cacheKey = config.hasOwnProperty("cacheKey") ? config.cacheKey : false;
|
38 |
|
39 | var gracePeriod = config.hasOwnProperty("gracePeriod") ? config.gracePeriod : 0;
|
40 | acceptField.gracePeriod = gracePeriod;
|
41 |
|
42 | var apiKey;
|
43 |
|
44 | productOnly = config.hasOwnProperty("productOnly") ? config.productOnly : false;
|
45 |
|
46 | if (process.env.EDGEMICRO_LOCAL_PROXY == "1") {
|
47 | productOnly = true;
|
48 | }
|
49 |
|
50 |
|
51 | if (apiKey = req.headers[apiKeyHeaderName]) {
|
52 | if (!keepApiKey) {
|
53 | delete(req.headers[apiKeyHeaderName]);
|
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)) {
|
71 | cache.read(apiKey, function(err, value) {
|
72 | if (value) {
|
73 | if (Date.now() / 1000 < value.exp) {
|
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)) {
|
198 |
|
199 | decodedToken.exp = decodedToken.exp || +(((Date.now() / 1000) + 1800).toFixed(0));
|
200 |
|
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 |
|
216 |
|
217 | const 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 |
|
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 |
|
269 |
|
270 | matchesProxyRules = urlPath == apiproxy;
|
271 |
|
272 | }
|
273 | }
|
274 | })
|
275 |
|
276 | } else {
|
277 | matchesProxyRules = true
|
278 | }
|
279 |
|
280 | debug("matches proxy rules: " + matchesProxyRules);
|
281 |
|
282 | if (!productOnly)
|
283 | return matchesProxyRules && validProxyNames.indexOf(proxy.name) >= 0;
|
284 | else
|
285 | return matchesProxyRules;
|
286 | });
|
287 | }
|
288 |
|
289 | function 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 |
|
301 | function 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 | }
|