UNPKG

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