UNPKG

16.4 kBJavaScriptView Raw
1/*!
2
3 ----------------------------------------------------------------------------
4 | qewd: Quick and Easy Web Development |
5 | |
6 | Copyright (c) 2017-19 M/Gateway Developments Ltd, |
7 | Redhill, Surrey UK. |
8 | All rights reserved. |
9 | |
10 | http://www.mgateway.com |
11 | Email: rtweed@mgateway.com |
12 | |
13 | |
14 | Licensed under the Apache License, Version 2.0 (the "License"); |
15 | you may not use this file except in compliance with the License. |
16 | You may obtain a copy of the License at |
17 | |
18 | http://www.apache.org/licenses/LICENSE-2.0 |
19 | |
20 | Unless required by applicable law or agreed to in writing, software |
21 | distributed under the License is distributed on an "AS IS" BASIS, |
22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
23 | See the License for the specific language governing permissions and |
24 | limitations under the License. |
25 ----------------------------------------------------------------------------
26
27 6 December 2019
28
29*/
30
31var jwt = require('jwt-simple');
32var crypto = require('crypto');
33//var algorithm = 'aes-256-ctr';
34var algorithm = 'aes-256-cbc';
35var key;
36var iv;
37
38function sizes(cipher) {
39 for (let nkey = 1, niv = 0;;) {
40 try {
41 crypto.createCipheriv(cipher, '.'.repeat(nkey), '.'.repeat(niv));
42 return [nkey, niv];
43 } catch (e) {
44 if (/invalid iv length/i.test(e.message)) niv += 1;
45 else if (/invalid key length/i.test(e.message)) nkey += 1;
46 else throw e;
47 }
48 }
49}
50
51function compute(cipher, passphrase) {
52 let [nkey, niv] = sizes(cipher);
53 for (let key = '', iv = '', p = '';;) {
54 const h = crypto.createHash('md5');
55 h.update(p, 'hex');
56 h.update(passphrase);
57 p = h.digest('hex');
58 let n, i = 0;
59 n = Math.min(p.length-i, 2*nkey);
60 nkey -= n/2, key += p.slice(i, i+n), i += n;
61 n = Math.min(p.length-i, 2*niv);
62 niv -= n/2, iv += p.slice(i, i+n), i += n;
63 if (nkey+niv === 0) return [key, iv];
64 }
65}
66
67function decrypt(text, secret) {
68 if (!key) {
69 var results = compute(algorithm, secret);
70 console.log('**** jwtHandler decrypt: key = ' + results[0]);
71 console.log('**** jwtHandler decrypt: iv = ' + results[1]);
72 key = Buffer.from(results[0], 'hex');
73 iv = Buffer.from(results[1], 'hex');
74 }
75 var dec;
76 try {
77 var decipher = crypto.createDecipheriv(algorithm, key, iv)
78 dec = decipher.update(text,'hex','utf8')
79 dec += decipher.final('utf8');
80 }
81 catch(err) {
82 dec = 'Error: ' + err;
83 }
84 return dec;
85}
86
87function encrypt(text, secret) {
88 if (!key) {
89 var results = compute(algorithm, secret);
90 console.log('**** jwtHandler encrypt: key = ' + results[0]);
91 console.log('**** jwtHandler encrypt: iv = ' + results[1]);
92 key = Buffer.from(results[0], 'hex');
93 iv = Buffer.from(results[1], 'hex');
94 }
95 var cipher = crypto.createCipheriv(algorithm, key, iv);
96 var crypted = cipher.update(text,'utf8','hex');
97 crypted += cipher.final('hex');
98 return crypted;
99}
100
101function decodeJWT(token) {
102 try {
103 return {payload: jwt.decode(token, this.jwt.secret)};
104 }
105 catch(err) {
106 return {error: 'Invalid JWT: ' + err};
107 }
108}
109
110function encodeJWT(payload) {
111 return jwt.encode(payload, this.jwt.secret);
112}
113
114function decodeJWTInWorker(jwt, callback) {
115 // decode / test the incoming JWT in a worker (to reduce master process CPU load)
116 var msg = {
117 type: 'ewd-jwt-decode',
118 params: {
119 jwt: jwt
120 }
121 };
122 this.handleMessage(msg, callback);
123}
124
125function encodeJWTInWorker(payload, callback) {
126 // encode the updated JWT in a worker (to reduce master process CPU load)
127 var msg = {
128 type: 'ewd-jwt-encode',
129 params: {
130 payload: payload
131 }
132 };
133 this.handleMessage(msg, callback);
134}
135
136function sendToMicroService(socketClient, data, application, handleResponse) {
137 var q = this;
138 //console.log('socketClient sending: ' + JSON.stringify(data));
139 socketClient.client.send(data, function(responseObj) {
140 if (!responseObj.message.error) {
141 if (responseObj.message.token) {
142 // reset the response JWT's application back to the one used in the original incoming JWT
143 var payload = jwt.decode(responseObj.message.token, null, true); // simple base64 decode
144 if (payload.application !== application) {
145 payload.application = application;
146 encodeJWTInWorker.call(q, payload, function(jwtObj) {
147 responseObj.message.token = jwtObj.message.jwt;
148 handleResponse(responseObj);
149 });
150 return;
151 }
152 }
153 }
154 handleResponse(responseObj);
155 });
156}
157
158function masterRequest(data, socket, handleResponse) {
159
160 // This runs in the master process, invoked by sockets.js
161
162 var q = this;
163 if (this.jwt && this.jwt.secret) {
164
165 // decode / test the incoming JWT in a worker (to reduce master process CPU load)
166
167 decodeJWTInWorker.call(this, data.token, function(responseObj) {
168 if (responseObj.message.error) {
169 socket.emit('ewdjs', {
170 type: data.type,
171 message: {
172 error: responseObj.message.error,
173 disconnect: true
174 }
175 });
176 return;
177 }
178
179 // incoming JWT was OK
180
181 data.jwt = true; // used by worker appHandler.js to recognise this as a JWT-based request
182
183 // is the incoming request to be handled locally or remotely using a registered microservice?
184 console.log('decodeJWTInWorker - responseObj = ' + JSON.stringify(responseObj));
185 var payload = responseObj.message.payload;
186 var application = payload.application;
187 if (q.u_services && q.u_services.byApplication[application] && q.u_services.byApplication[application][data.type]) {
188
189 if (q.log) console.log('incoming request for ' + application + ': ' + data.type + ' will be handled by microservice');
190
191 // if application has a microservice defined for the incoming message type,
192 // re-direct it to the handling server
193 // handle the response using handleResponse()
194 // and return
195
196 // need to rewrite the JWT to change to the micro-service system's application name
197
198 var socketClient = q.u_services.byApplication[application][data.type];
199
200 if (payload.application !== socketClient.application) {
201 payload.application = socketClient.application;
202
203 // encode the updated JWT in a worker (to reduce master process CPU load)
204
205 encodeJWTInWorker.call(q, payload, function(responseObj) {
206 data.token = responseObj.message.jwt;
207 sendToMicroService.call(q, socketClient, data, application, handleResponse)
208 });
209 }
210 else {
211 sendToMicroService.call(q, socketClient, data, application, handleResponse)
212 }
213 }
214 else {
215 // handle the request locally
216
217 // send to worker process for handling - see appHandler.js
218
219 q.handleMessage(data, handleResponse);
220 }
221 });
222 }
223 else {
224 // QEWD has not been started with JWT support turned on
225
226 socket.emit('ewdjs', {
227 type: data.type,
228 message: {
229 error: 'QEWD has not been configured to support JWTs',
230 disconnect: true
231 }
232 });
233 return;
234 }
235}
236
237function register(messageObj) {
238
239 if (!this.jwt) {
240 // QEWD isn't running with JWTs enabled
241 delete messageObj.jwt;
242 return {
243 error: 'Application expects to use JWTs, but QEWD is not running with JWT support turned on',
244 disconnect: true
245 };
246 }
247
248 return {token: createJWT.call(this, messageObj)};
249}
250
251function reregister(payload, messageObj) {
252 payloadsocketId = messageObj.socketId;
253 var token = updateJWT.call(this, payload);
254 return {
255 ok: true,
256 token: token
257 };
258}
259
260function createUServiceSession(messageObj) {
261 return createRestSession.call(this, {
262 req: {
263 application: messageObj.application,
264 ip: messageObj.ip
265 }
266 });
267}
268
269function createRestSession(args, timeout) {
270 var now = Math.floor(Date.now()/1000);
271 var timeout = timeout || this.userDefined.config.initialSessionTimeout;
272
273 var payload = {
274 exp: now + timeout,
275 iat: now,
276 iss: 'qewd.jwt',
277 application: args.req.application,
278 ipAddress: args.req.ip,
279 timeout: timeout,
280 authenticated: false,
281 qewd: {
282 },
283 qewd_list: {
284 ipAddress: true,
285 authenticated: true
286 }
287 };
288
289 // add JWT session helper functions
290
291 payload.makeSecret = function(name) {
292 payload.qewd_list[name] = true;
293 }
294 payload.isSecret = function(name) {
295 if (payload.qewd_list[name] === true) return true;
296 return false;
297 }
298 payload.makePublic = function(name) {
299 delete payload.qewd_list[name];
300 }
301 return payload;
302
303}
304
305function createJWT(messageObj) {
306
307 var now = Math.floor(Date.now()/1000);
308 var timeout = this.userDefined.config.initialSessionTimeout;
309
310 var payload = {
311 exp: now + timeout,
312 iat: now,
313 iss: 'qewd.jwt',
314 application: messageObj.application,
315 timeout: timeout
316 };
317
318 var secretData = {
319 socketId: messageObj.socketId,
320 ipAddress: messageObj.ipAddress,
321 authenticated: false
322 };
323
324 payload.qewd = encrypt(JSON.stringify(secretData), this.jwt.secret);
325
326 return jwt.encode(payload, this.jwt.secret);
327}
328
329function validate(messageObj, noVerify) {
330 //console.log('** validate using JWT: ' + messageObj.token);
331
332 if (noVerify === true) {
333 return {
334 session: jwt.decode(messageObj.token, 'dummy', true)
335 }
336 }
337
338 try {
339 var payload = jwt.decode(messageObj.token, this.jwt.secret);
340 }
341 catch(err) {
342 return {
343 error: 'Invalid JWT: ' + err,
344 status: {
345 code: 403,
346 text: 'Forbidden'
347 }
348 };
349 }
350 // extract the secret data
351
352 var dec = decrypt(payload.qewd, this.jwt.secret);
353 if (typeof dec === 'string' && dec.startsWith('Error: ')) {
354 console.log("\n*** Unable to decrypt the JWT's secret payload, possibly due to ");
355 console.log('an invalid or outdated cookie in the browser, eg before a change');
356 console.log('of JWT secret. Delete the browser cookie');
357 console.log(dec);
358 console.log('-----');
359 dec = {};
360 }
361 try {
362 payload.qewd = JSON.parse(dec);
363 payload.qewd_list = {};
364 for (var name in payload.qewd) {
365 // transfer into payload top-level for back-end use
366 // (will be removed again before returning updated JWT to client)
367
368 if (!payload[name]) {
369 payload[name] = payload.qewd[name];
370 payload.qewd_list[name] = true;
371 }
372 }
373 }
374 catch(err) {
375 // leave enc property alone
376 }
377
378 // add JWT session helper functions
379
380 payload.makeSecret = function(name) {
381 payload.qewd_list[name] = true;
382 }
383 payload.isSecret = function(name) {
384 if (payload.qewd_list[name] === true) return true;
385 return false;
386 }
387 payload.makePublic = function(name) {
388 delete payload.qewd_list[name];
389 }
390 //console.log('*** session payload = ' + JSON.stringify(payload));
391 return {
392 session: payload
393 };
394}
395
396function getProperty(propertyName, token) {
397 var payload;
398 try {
399 payload = jwt.decode(token, null, true);
400 }
401 catch(err) {
402 return false;
403 }
404 if (!payload[propertyName]) return false;
405 return payload[propertyName];
406}
407
408function updateJWTExpiry(token, application) {
409 try {
410 var payload = jwt.decode(token, null, true);
411 }
412 catch(err) {
413 return false;
414 }
415
416 // update the expiry time
417
418 //console.log('*** updating expiry for payload ' + JSON.stringify(payload));
419
420 var now = Math.floor(Date.now()/1000);
421 payload.iat = now;
422 payload.exp = now + payload.timeout || 300;
423 if (application && application !== '') payload.application = application;
424 //console.log('JWT expiry updated - now = ' + now + '; exp = ' + payload.exp);
425 token = jwt.encode(payload, this.jwt.secret);
426 return token;
427}
428
429function updateJWT(payload) {
430
431 var timeout = payload.timeout;
432
433 //console.log('updateJWT: payload = ' + JSON.stringify(payload));
434
435 if (payload.qewd) {
436 for (var name in payload.qewd_list) {
437 //console.log('qewd_list ' + name);
438 // transfer qewd-only values into qewd property for encryption
439 if (typeof payload[name] !== 'undefined') {
440 //console.log('updateJWT: name = ' + name + '; value = ' + payload[name]);
441 payload.qewd[name] = payload[name];
442 delete payload[name];
443 }
444 }
445 // encrypt the secret data and save as payload.qewd
446 payload.qewd = encrypt(JSON.stringify(payload.qewd), this.jwt.secret);
447
448 // remove the helper functions from the payload/session
449
450 delete payload.qewd_list;
451 delete payload.makeSecret;
452 delete payload.makePublic;
453 delete payload.isSecret;
454 }
455
456 // update the expiry time
457
458 var now = Math.floor(Date.now()/1000);
459 payload.iat = now;
460 payload.exp = now + timeout;
461 var token = jwt.encode(payload, this.jwt.secret);
462 return token;
463};
464
465
466function validateRestRequest(messageObj, finished, bearer, checkIfAuthenticated) {
467 if (checkIfAuthenticated !== false) checkIfAuthenticated = true; // has to be explicitly false to override
468 var jwt = getRestJWT(messageObj, bearer);
469 if (jwt === '') {
470 finished({error: 'Authorization Header missing or JWT not found in header (expected format: Bearer {{JWT}}'});
471 return false;
472 }
473 //console.log('**** jwt = ' + jwt);
474 var status = validate.call(this, {token: jwt});
475 if (status.error) {
476 finished(status);
477 return false;
478 }
479 if (checkIfAuthenticated && !status.session.authenticated) {
480 finished({error: 'User is not authenticated'});
481 return false;
482 }
483 messageObj.session = status.session;
484 return true;
485}
486
487function getRestJWT(messageObj, bearer) {
488 var jwt = '';
489 if (messageObj.headers && messageObj.headers.authorization) {
490 jwt = messageObj.headers.authorization;
491 if (bearer !== false) {
492 jwt = jwt.split('Bearer ')[1];
493 if (typeof jwt === 'undefined') jwt = '';
494 }
495 }
496 return jwt;
497}
498
499function isTokenAPossibleJWT(token) {
500 var pieces = token.split('.');
501 if (pieces.length !== 3) return false;
502 if (pieces[0].length < 2) return false;
503 if (pieces[1].length < 2) return false;
504 if (pieces[2].length < 2) return false;
505 return true;
506}
507
508function isJWTValid(token, noVerify) {
509 try {
510 var payload = jwt.decode(token, this.jwt.secret, noVerify);
511 if (noVerify) {
512 if (payload.exp && payload.exp < (Date.now()/1000)) {
513 return {
514 ok: false,
515 error: 'Invalid JWT: Token expired'
516 };
517 }
518 }
519 return {ok: true};
520 }
521 catch(err) {
522 return {
523 ok: false,
524 error: 'Invalid JWT: ' + err
525 };
526 }
527}
528
529module.exports = {
530 masterRequest: masterRequest,
531 register: register,
532 reregister: reregister,
533 createJWT: createJWT,
534 createRestSession: createRestSession,
535 updateJWT: updateJWT,
536 setJWT: updateJWT,
537 validate: validate,
538 getRestJWT: getRestJWT,
539 validateRestRequest: validateRestRequest,
540 updateJWTExpiry: updateJWTExpiry,
541 isJWTValid: isJWTValid,
542 createUServiceSession: createUServiceSession,
543 decodeJWT: decodeJWT,
544 encodeJWT: encodeJWT,
545 getProperty: getProperty
546};