1 | 'use strict';
|
2 |
|
3 | var debug = require('debug')('plugin:oauthv2');
|
4 | var url = require('url');
|
5 | var rs = require('jsrsasign');
|
6 | const memoredpath = '../third_party/memored/index';
|
7 | const checkIfAuthorized =require('../lib/validateResourcePath');
|
8 | var map = require(memoredpath);
|
9 | var JWS = rs.jws.JWS;
|
10 |
|
11 | var _ = require('lodash');
|
12 |
|
13 | const authHeaderRegex = /Bearer (.+)/;
|
14 | const PRIVATE_JWT_VALUES = ['application_name', 'client_id', 'api_product_list', 'iat', 'exp'];
|
15 | const LOG_TAG_COMP = 'oauthv2';
|
16 |
|
17 | const acceptAlg = ['RS256'];
|
18 |
|
19 | var acceptField = {};
|
20 | acceptField.alg = acceptAlg;
|
21 |
|
22 | var productOnly;
|
23 |
|
24 | var tokenCache = false;
|
25 | map.setup({
|
26 | purgeInterval: 10000
|
27 | });
|
28 |
|
29 | var tokenCacheSize = 100;
|
30 |
|
31 | module.exports.init = function(config, logger, stats) {
|
32 |
|
33 | if (config === undefined || !config) return (undefined);
|
34 |
|
35 |
|
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 |
|
43 | var gracePeriod = config.hasOwnProperty('gracePeriod') ? config.gracePeriod : 0;
|
44 | acceptField.gracePeriod = gracePeriod;
|
45 |
|
46 | productOnly = config.hasOwnProperty('productOnly') ? config.productOnly : false;
|
47 |
|
48 | if (process.env.EDGEMICRO_LOCAL_PROXY === "1") {
|
49 | productOnly = true;
|
50 | }
|
51 |
|
52 | tokenCache = config.hasOwnProperty('tokenCache') ? config.tokenCache : false;
|
53 |
|
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]);
|
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 |
|
196 | },
|
197 |
|
198 |
|
199 | testing: {
|
200 | ejectToken
|
201 | }
|
202 | };
|
203 |
|
204 | }
|
205 |
|
206 | function 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 |
|
219 | function ejectToken(expTimestampInSeconds) {
|
220 | var currentTimestampInSeconds = new Date().getTime() / 1000;
|
221 | var gracePeriod = parseInt(acceptField.gracePeriod)
|
222 | return currentTimestampInSeconds > expTimestampInSeconds + gracePeriod
|
223 | }
|
224 |
|
225 | function 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 |
|
252 | function 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 |
|
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 | }
|