UNPKG

10.6 kBJavaScriptView Raw
1'use strict';
2
3var debug = require('debug')('plugin:oauthv2');
4var url = require('url');
5var rs = require('jsrsasign');
6const memoredpath = '../third_party/memored/index';
7const checkIfAuthorized =require('../lib/validateResourcePath');
8var map = require(memoredpath);
9var JWS = rs.jws.JWS;
10//var requestLib = require('request');
11var _ = require('lodash');
12
13const authHeaderRegex = /Bearer (.+)/;
14const PRIVATE_JWT_VALUES = ['application_name', 'client_id', 'api_product_list', 'iat', 'exp'];
15const LOG_TAG_COMP = 'oauthv2';
16
17const acceptAlg = ['RS256'];
18
19var acceptField = {};
20acceptField.alg = acceptAlg;
21
22var productOnly;
23//setup cache for oauth tokens
24var tokenCache = false;
25map.setup({
26 purgeInterval: 10000
27});
28
29var tokenCacheSize = 100;
30
31module.exports.init = function(config, logger, stats) {
32
33 if (config === undefined || !config) return (undefined);
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 var header = false;
57 if (!req.headers[authHeaderName]) {
58 if (config.allowNoAuthorization) {
59 return next();
60 } else {
61 debug('missing_authorization');
62 return sendError(req, res, next, logger, stats, 'missing_authorization', 'Missing Authorization header');
63 }
64 } else {
65 header = authHeaderRegex.exec(req.headers[authHeaderName]);
66 if (!(header) || (header.length < 2) ) {
67 debug('Invalid Authorization Header');
68 return sendError(req, res, next, logger, stats, 'invalid_request', 'Invalid Authorization header');
69 }
70 }
71
72 if (!keepAuthHeader) {
73 delete(req.headers[authHeaderName]); // don't pass this header to target
74 }
75
76 var token = '';
77 if (header) {
78 token = header[1];
79 }
80 verify(token, config, logger, stats, middleware, req, res, next);
81 }
82
83 var verify = function(token, config, logger, stats, middleware, req, res, next) {
84
85 var isValid = false;
86 var oauthtoken = token && token.token ? token.token : token;
87 var decodedToken;
88 try {
89 decodedToken = JWS.parse(oauthtoken);
90 } catch (err) {
91 if (config.allowInvalidAuthorization) {
92 logger.eventLog({level:'warn', req: req, res: res, err: err, component:LOG_TAG_COMP }, 'ignoring err');
93 return next();
94 } else {
95 debug('invalid token');
96 return sendError(req, res, next, logger, stats, 'invalid_token', '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 try {
113 if (keys) {
114 debug('using jwk');
115 var pem = getPEM(decodedToken, keys);
116 isValid = JWS.verifyJWT(oauthtoken, pem, acceptField);
117 } else {
118 debug('validating jwt');
119 isValid = JWS.verifyJWT(oauthtoken, config.public_key, acceptField);
120 }
121 } catch (error) {
122 logger.eventLog({level:'warn', req: req, res: res, err:null, component:LOG_TAG_COMP }, 'error parsing jwt: ' + oauthtoken);
123 }
124 }
125 if (!isValid) {
126 if (config.allowInvalidAuthorization) {
127 logger.eventLog({level:'warn', req: req, res: res, err:null, component:LOG_TAG_COMP }, 'ignoring err in verify');
128 return next();
129 } else {
130 debug('invalid token');
131 return sendError(req, res, next, logger, stats,'invalid_token', 'invalid_token');
132 }
133 } else {
134 if (tokenvalue === null || tokenvalue === undefined) {
135 map.size(function(err, sizevalue) {
136 if (!err && sizevalue !== null && sizevalue < tokenCacheSize) {
137 map.store(oauthtoken, oauthtoken, decodedToken.payloadObj.exp);
138 } else {
139 debug('too many tokens in cache; ignore storing token');
140 }
141 });
142 }
143 authorize(req, res, next, logger, stats, decodedToken.payloadObj);
144 }
145 });
146 } else {
147 try {
148 if (keys) {
149 debug('using jwk');
150 var pem = getPEM(decodedToken, keys);
151 isValid = JWS.verifyJWT(oauthtoken, pem, acceptField);
152 } else {
153 debug('validating jwt');
154 isValid = JWS.verifyJWT(oauthtoken, config.public_key, acceptField);
155 }
156 } catch (error) {
157 logger.eventLog({level:'warn', req: req, res: res, err:null, component:LOG_TAG_COMP }, 'error parsing jwt: ' + oauthtoken);
158 }
159 if (!isValid) {
160 if (config.allowInvalidAuthorization) {
161 logger.eventLog({level:'warn', req: req, res: res, err:null, component:LOG_TAG_COMP }, 'ignoring err in verify');
162 return next();
163 } else {
164 debug('invalid token');
165 return sendError(req, res, next, logger, stats, 'invalid_token', 'invalid_token');
166 }
167 } else {
168 authorize(req, res, next, logger, stats, decodedToken.payloadObj);
169 }
170 }
171 };
172
173 function authorize(req, res, next, logger, stats, decodedToken) {
174 if (checkIfAuthorized(config, req, res, decodedToken, productOnly, logger, LOG_TAG_COMP)) {
175 req.token = decodedToken;
176 var authClaims = _.omit(decodedToken, PRIVATE_JWT_VALUES);
177 req.headers['x-authorization-claims'] = new Buffer(JSON.stringify(authClaims)).toString('base64');
178 next();
179 } else {
180 return sendError(req, res, next, logger, stats, 'access_denied', 'access_denied');
181 }
182 }
183
184 return {
185 onrequest: function(req, res, next) {
186 if (process.env.EDGEMICRO_LOCAL === "1") {
187 debug ("MG running in local mode. Skipping OAuth");
188 next();
189 } else {
190 middleware(req, res, next);
191 }
192 },
193
194 shutdown() {
195 // tests are needing shutdowns to remove services that keep programs running, etc.
196 },
197
198 // specifically a way of exporting support routines for coverage testing
199 testing: {
200 ejectToken
201 }
202 };
203
204}
205
206function getPEM(decodedToken, keys) {
207 var i = 0;
208 debug('jwk kid ' + decodedToken.headerObj.kid);
209 for (; i < keys.length; i++) {
210 if (keys.kid === decodedToken.headerObj.kid) {
211 break;
212 }
213 }
214 var publickey = rs.KEYUTIL.getKey(keys.keys[i]);
215 return rs.KEYUTIL.getPEM(publickey);
216}
217
218// this should be in a separate module. This code is being copied from instance to instance.
219function ejectToken(expTimestampInSeconds) {
220 var currentTimestampInSeconds = new Date().getTime() / 1000;
221 var gracePeriod = parseInt(acceptField.gracePeriod)
222 return currentTimestampInSeconds > expTimestampInSeconds + gracePeriod
223}
224
225function setResponseCode(res,code) {
226 switch ( code ) {
227 case 'invalid_request': {
228 res.statusCode = 400;
229 break;
230 }
231 case 'access_denied':{
232 res.statusCode = 403;
233 break;
234 }
235 case 'invalid_token':
236 case 'missing_authorization':
237 case 'invalid_authorization': {
238 res.statusCode = 401;
239 break;
240 }
241 case 'gateway_timeout': {
242 res.statusCode = 504;
243 break;
244 }
245 default: {
246 res.statusCode = 500;
247 break;
248 }
249 }
250}
251
252function sendError(req, res, next, logger, stats, code, message) {
253
254 setResponseCode(res,code);
255
256 var response = {
257 error: code,
258 error_description: message
259 };
260 const err = Error(message)
261 debug('auth failure', res.statusCode, code, message ? message : '', req.headers, req.method, req.url);
262 logger.eventLog({level:'error', req: req, res: res, err:err, component:LOG_TAG_COMP }, message);
263
264 //opentracing
265 if (process.env.EDGEMICRO_OPENTRACE) {
266 try {
267 const traceHelper = require('../microgateway-core/lib/trace-helper');
268 traceHelper.setChildErrorSpan('oauthv2', req.headers);
269 } catch (err) {}
270 }
271 //
272
273 if (!res.finished) res.setHeader('content-type', 'application/json');
274 res.end(JSON.stringify(response));
275 stats.incrementStatusCount(res.statusCode);
276 next(code, message);
277 return code;
278}