@tiledesk/tiledesk-server 2.22.7 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,12 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
- # 2.22.0
8
+ # 2.23.1
9
+ - Deprecated JWT helper routes (`/jwt/decode`, `/jwt/generatetestjwt`); they now return 410
10
+ - Restricted bot and subscription tokens to their own project (cross-project access denied)
11
+ - Improved error response sanitization for integration routes
12
+
13
+ # 2.22.7
9
14
  - Addressed and resolved a security vulnerability
10
15
  - Updated tybot-connector to 2.1.11
11
16
 
package/app.js CHANGED
@@ -622,7 +622,7 @@ app.use('/:projectid/publicanalytics', publicAnalytics);
622
622
 
623
623
  app.use('/:projectid/keys', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('agent')], key);
624
624
 
625
- //TODO deprecated?
625
+ // Deprecated helper routes (/jwt/decode, /jwt/generatetestjwt) — respond with 410.
626
626
  app.use('/:projectid/jwt', jwtroute);
627
627
 
628
628
 
@@ -36,14 +36,32 @@ class RoleChecker {
36
36
 
37
37
 
38
38
 
39
- isTypeAsFunction(type, user) {
39
+ /**
40
+ * Bot and subscription tokens must only authorize the project they belong to.
41
+ * Fail closed if id_project is missing.
42
+ */
43
+ belongsToProject(user, projectid) {
44
+ if (!user || user.id_project == null || projectid == null) {
45
+ return false;
46
+ }
47
+ return String(user.id_project) === String(projectid);
48
+ }
49
+
50
+ isTypeAsFunction(type, user, projectid) {
40
51
  winston.debug("isType:"+type);
41
52
  winston.debug("user", user);
42
- //TODO Check if belongs to project
43
53
  if (type=='subscription' && user instanceof Subscription){
54
+ if (!this.belongsToProject(user, projectid)) {
55
+ winston.warn("Subscription " + (user && user._id) + " denied cross-project access to " + projectid);
56
+ return false;
57
+ }
44
58
  winston.debug("isTypeAsFunction is subscription");
45
59
  return true
46
60
  } else if (type=='bot' && user instanceof Faq_kb){
61
+ if (!this.belongsToProject(user, projectid)) {
62
+ winston.warn("Bot " + (user && user._id) + " denied cross-project access to " + projectid);
63
+ return false;
64
+ }
47
65
  winston.debug("isTypeAsFunction is bot");
48
66
  return true
49
67
  } else {
@@ -51,7 +69,7 @@ class RoleChecker {
51
69
 
52
70
  var adminEmail = process.env.ADMIN_EMAIL || "admin@tiledesk.com";
53
71
 
54
- if (user && user.email && user.email === adminEmail) { //skip has role check
72
+ if (user && user.email && user.email === adminEmail) { //skip has role check
55
73
  return true;
56
74
  }
57
75
  return false;
@@ -59,7 +77,7 @@ class RoleChecker {
59
77
  }
60
78
 
61
79
 
62
- isTypesAsFunction(types, user) {
80
+ isTypesAsFunction(types, user, projectid) {
63
81
  winston.debug("isTypes:"+types);
64
82
  winston.debug("user", user);
65
83
  var isType = false;
@@ -69,7 +87,7 @@ class RoleChecker {
69
87
  try {
70
88
  types.forEach(type => {
71
89
  winston.debug("type:"+type);
72
- isType = this.isTypeAsFunction(type, user);
90
+ isType = this.isTypeAsFunction(type, user, projectid);
73
91
  winston.debug("isType:"+ isType);
74
92
  if (isType==true) {
75
93
  throw BreakException; //https://stackoverflow.com/questions/2641347/short-circuit-array-foreach-like-calling-break
@@ -191,12 +209,12 @@ class RoleChecker {
191
209
  // console.log("QUIIIIIIIIIIIIIIIIIIIIIII",type);
192
210
  if (types && types.length>0) {
193
211
  // console.log("QUIIIIIIIIIIIIIIIIIIIIIII");
194
- var checkRes = that.isTypesAsFunction(types, req.user);
212
+ var checkRes = that.isTypesAsFunction(types, req.user, projectid);
195
213
  winston.debug("checkRes: " + checkRes);
196
214
 
197
215
  if (checkRes) {
198
216
  return next();
199
- }
217
+ }
200
218
  // return that.isType(type)(req,res,next);
201
219
  // console.log("typers",typers);
202
220
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiledesk/tiledesk-server",
3
3
  "description": "The Tiledesk server module",
4
- "version": "2.22.7",
4
+ "version": "2.23.0",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -52,7 +52,7 @@ router.get('/:integration_id', async (req, res) => {
52
52
  Integration.findOne({ _id: integration_id, id_project: id_project }, (err, integration) => {
53
53
  if (err) {
54
54
  winston.error("Error find integration by id: ", err);
55
- return res.status(404).send({ success: false, err: err });
55
+ return res.status(500).send({ success: false, message: "Error getting integration" });
56
56
  }
57
57
  if (!integration) {
58
58
  return res.status(404).send({ success: false, message: "Integration not found" });
@@ -73,7 +73,7 @@ router.get('/name/:integration_name', async (req, res) => {
73
73
  Integration.findOne({ id_project: id_project, name: integration_name }, (err, integration) => {
74
74
  if (err) {
75
75
  winston.error("Error find integration by name: ", err);
76
- return res.status(404).send({ success: false, err: err });
76
+ return res.status(500).send({ success: false, message: "Error getting integration" });
77
77
  }
78
78
 
79
79
  if (!integration) {
@@ -96,7 +96,7 @@ router.post('/', async (req, res) => {
96
96
  existingIntegration = await Integration.findOne({ id_project: id_project, name: req.body.name });
97
97
  } catch (err) {
98
98
  winston.error("Error finding existing integration ", err);
99
- return res.status(500).send({ success: false, err: err });
99
+ return res.status(500).send({ success: false, message: "Error creating integration" });
100
100
  }
101
101
 
102
102
  let newIntegration = {
@@ -112,7 +112,7 @@ router.post('/', async (req, res) => {
112
112
  Integration.findOneAndUpdate({ id_project: id_project, name: req.body.name }, newIntegration, { new: true, upsert: true, setDefaultsOnInsert: false}, (err, savedIntegration) => {
113
113
  if (err) {
114
114
  winston.error("Error creating new integration ", err);
115
- return res.status(404).send({ success: false, err: err })
115
+ return res.status(500).send({ success: false, message: "Error creating integration" });
116
116
  }
117
117
 
118
118
  winston.debug("New integration created: ", savedIntegration);
@@ -168,7 +168,7 @@ router.put('/:integration_id', async (req, res) => {
168
168
  existingIntegration = await Integration.findOne({ _id: integration_id, id_project: id_project });
169
169
  } catch (err) {
170
170
  winston.error("Error finding integration to update ", err);
171
- return res.status(500).send({ success: false, error: err });
171
+ return res.status(500).send({ success: false, message: "Error updating integration" });
172
172
  }
173
173
  if (!existingIntegration) {
174
174
  return res.status(404).send({ success: false, message: "Integration not found" });
@@ -187,7 +187,7 @@ router.put('/:integration_id', async (req, res) => {
187
187
  Integration.findOneAndUpdate({ _id: integration_id, id_project: id_project }, update, { new: true }, (err, savedIntegration) => {
188
188
  if (err) {
189
189
  winston.error("Error find by id and update integration: ", err);
190
- return res.status(500).send({ success: false, error: err })
190
+ return res.status(500).send({ success: false, message: "Error updating integration" });
191
191
  }
192
192
  if (!savedIntegration) {
193
193
  return res.status(404).send({ success: false, message: "Integration not found" });
@@ -213,7 +213,7 @@ router.delete('/:integration_id', async (req, res) => {
213
213
  Integration.findOneAndDelete({ _id: integration_id, id_project: id_project }, (err, result) => {
214
214
  if (err) {
215
215
  winston.error("Error find by id and delete integration: ", err);
216
- return res.status(500).send({ success: false, error: err })
216
+ return res.status(500).send({ success: false, message: "Error deleting integration" });
217
217
  }
218
218
  if (!result) {
219
219
  return res.status(404).send({ success: false, message: "Integration not found" });
package/routes/jwt.js CHANGED
@@ -4,32 +4,35 @@
4
4
  // Modules imports
5
5
  const express = require('express');
6
6
  var router = express.Router();
7
- var config = require('../config/database');
7
+
8
+ // Deprecated test/helper endpoints. Kept mounted so callers get an explicit
9
+ // deprecation response instead of a silent 404. Do not re-enable without
10
+ // proper authentication (passport + role checks).
11
+ function deprecated(req, res) {
12
+ return res.status(410).send({
13
+ success: false,
14
+ msg: 'deprecated'
15
+ });
16
+ }
17
+
18
+ router.post('/decode', deprecated);
19
+ router.post('/generatetestjwt', deprecated);
20
+
21
+ /*
22
+ var config = require('../config/database');
8
23
  var jwt = require('jsonwebtoken');
9
24
  var validtoken = require('../middleware/valid-token');
10
25
  var requestUtil = require('../utils/requestUtil');
11
26
  var Project = require('../models/project');
12
27
  var winston = require('../config/winston');
13
28
 
14
- // deprecated??
15
29
  router.post('/decode', validtoken, function (req, res) {
16
-
17
- // var project_id = req.query.project_id;
18
-
19
- // winston.debug("project_id", project_id);
20
-
21
- // if (!project_id) {
22
- // winston.error("project_id parameter is required");
23
- // res.status(400).send({ success: false, msg: "project_id parameter is required" });
24
- // }
25
-
26
-
27
- return Project.findOne({_id: req.projectid, status: 100}, '+jwtSecret',function(err, project) {
30
+ return Project.findOne({_id: req.projectid, status: 100}, '+jwtSecret',function(err, project) {
28
31
  if (err) {
29
32
  winston.error('Error finding project', err);
30
33
  return res.status(500).send({ success: false, msg: 'Error finding project.' });
31
34
  }
32
-
35
+
33
36
  winston.debug('project', project);
34
37
 
35
38
  if (!project) {
@@ -40,74 +43,60 @@ router.post('/decode', validtoken, function (req, res) {
40
43
  return res.status(404).send({ success: false, msg: 'Project hasnt a jwtSecret' });
41
44
  }
42
45
 
43
-
44
46
  try {
45
47
  winston.debug("requestUtil", requestUtil);
46
48
  var token = requestUtil.getToken(req.headers);
47
49
 
48
-
49
50
  winston.debug("token", token);
50
- // verify a token symmetric - synchronous
51
51
  var decoded = jwt.verify(token, project.jwtSecret);
52
-
52
+
53
53
  winston.debug("decoded", decoded);
54
-
55
54
 
56
- if(!decoded.iat ) {
55
+ if(!decoded.iat ) {
57
56
  winston.debug("token.iat is required");
58
57
  return res.status(401).send({ success: false, msg: 'Authentication failed. Token iat is required'});
59
58
  }
60
- if(!decoded.exp ) {
59
+ if(!decoded.exp ) {
61
60
  winston.debug("token.exp is required");
62
61
  return res.status(401).send({ success: false, msg: 'Authentication failed. Token exp is required'});
63
62
  }
64
- if(decoded.exp - decoded.iat > 600 ) {
63
+ if(decoded.exp - decoded.iat > 600 ) {
65
64
  winston.debug("The value of exp is permitted to be up to a maximum of 10 minutes from the iat value");
66
65
  return res.status(401).send({ success: false, msg: 'Authentication failed. The value of exp is permitted to be up to a maximum of 10 minutes from the iat value'});
67
66
  }
68
67
 
69
-
70
68
  return res.json({decoded: decoded});
71
-
72
-
69
+
73
70
  } catch(err) {
74
71
  winston.error("Authentication failed", err);
75
72
  return res.status(401).send({ success: false, msg: 'Authentication failed', err: err });
76
- }
73
+ }
77
74
 
78
75
  });
79
-
80
-
81
76
  });
82
77
 
83
78
  router.post('/generatetestjwt', validtoken, function (req, res) {
84
-
85
79
  winston.debug("req.body", req.body);
86
- return Project.findOne({_id: req.projectid, status: 100}, '+jwtSecret',function(err, project) {
80
+ return Project.findOne({_id: req.projectid, status: 100}, '+jwtSecret',function(err, project) {
87
81
  if (err) {
88
82
  winston.error('Error finding project', err);
89
83
  return res.status(500).send({ success: false, msg: 'Error finding project.' });
90
84
  }
91
-
85
+
92
86
  winston.debug('project', project);
93
-
87
+
94
88
  if (!project) {
95
89
  return res.status(404).send({ success: false, msg: 'Project not found' });
96
90
  }
97
-
91
+
98
92
  if (!project.jwtSecret) {
99
93
  return res.status(404).send({ success: false, msg: 'Project hasnt a jwtSecret' });
100
94
  }
101
95
 
102
96
  var token = jwt.sign(req.body, project.jwtSecret,{ expiresIn: 300 });
103
- // var token = jwt.sign(req.body, project.jwtSecret);
104
-
105
-
106
-
107
97
  res.json({ success: true, token: 'JWT ' + token });
108
98
  });
109
-
110
99
  });
100
+ */
111
101
 
112
-
113
- module.exports = router;
102
+ module.exports = router;
@@ -401,7 +401,41 @@ describe('AuthenticationJWT', () => {
401
401
  });
402
402
  });
403
403
  });
404
- });
404
+ });
405
+
406
+ it('botJwt-denied-cross-project', (done) => {
407
+
408
+ var email = "test-signup-" + Date.now() + "@email.com";
409
+ var pwd = "pwd";
410
+
411
+ userService.signup(email, pwd, "Test Firstname", "Test lastname").then(function (savedUser) {
412
+ projectService.create("test-bot-project-a", savedUser._id).then(function (projectA) {
413
+ projectService.create("test-bot-project-b", savedUser._id).then(function (projectB) {
414
+ faqService.create(projectA._id, savedUser._id, { name: "testbot-cross" }).then(function (savedBot) {
415
+
416
+ var savedBotObj = savedBot.toObject();
417
+ var signOptions = {
418
+ subject: 'bot',
419
+ audience: 'https://tiledesk.com/bots/' + savedBot._id,
420
+ };
421
+ var jwtToken = jwt.sign(savedBotObj, savedBot.secret, signOptions);
422
+
423
+ chai.request(server)
424
+ .get('/' + projectB._id + '/authtestWithRoleCheck/bot')
425
+ .set('Authorization', 'JWT ' + jwtToken)
426
+ .send()
427
+ .end((err, res) => {
428
+ if (err) { console.error("err: ", err); }
429
+ if (log) { console.log("res.body", res.body); }
430
+
431
+ res.should.have.status(403);
432
+ done();
433
+ });
434
+ });
435
+ });
436
+ });
437
+ });
438
+ });
405
439
 
406
440
  });
407
441
 
package/test/jwtRoute.js CHANGED
@@ -1,7 +1,6 @@
1
1
  //During the test the env variable is set to test
2
2
  process.env.NODE_ENV = 'test';
3
3
 
4
- var User = require('../models/user');
5
4
  var projectService = require('../services/projectService');
6
5
  var userService = require('../services/userService');
7
6
 
@@ -10,158 +9,60 @@ let chai = require('chai');
10
9
  let chaiHttp = require('chai-http');
11
10
  let server = require('../app');
12
11
  let should = chai.should();
13
- var jwt = require('jsonwebtoken');
14
12
 
15
13
  let log = false;
16
14
 
17
- // chai.config.includeStack = true;
18
-
19
15
  var expect = chai.expect;
20
- var assert = chai.assert;
21
16
 
22
17
  chai.use(chaiHttp);
23
18
 
24
19
  describe('JWTRoute', () => {
25
20
 
26
- describe('/decode', () => {
27
-
28
- it('decodeWithIatExp', (done) => {
21
+ describe('deprecated endpoints', () => {
29
22
 
23
+ it('generatetestjwt returns deprecated', (done) => {
30
24
  var email = "test-signup-" + Date.now() + "@email.com";
31
25
  var pwd = "pwd";
32
26
 
33
27
  userService.signup(email, pwd, "Test Firstname", "Test lastname").then(function (savedUser) {
34
- projectService.create("test-join-member", savedUser._id).then(function (savedProject) {
28
+ projectService.create("test-jwt-deprecated", savedUser._id).then(function (savedProject) {
35
29
  chai.request(server)
36
- .post('/' + savedProject._id + '/keys/generate')
30
+ .post('/' + savedProject._id + '/jwt/generatetestjwt')
37
31
  .auth(email, pwd)
38
- .send()
32
+ .send({ "name": "andrea", "surname": "leo" })
39
33
  .end((err, res) => {
40
-
41
34
  if (err) { console.error("err: ", err); }
42
35
  if (log) { console.log("res.body", res.body); }
43
36
 
44
- res.should.have.status(200);
37
+ res.should.have.status(410);
45
38
  res.body.should.be.a('object');
46
- expect(res.body.jwtSecret).to.not.equal(null);
47
-
48
- chai.request(server)
49
- .post('/' + savedProject._id + '/jwt/generatetestjwt')
50
- .auth(email, pwd)
51
- .send({ "name": "andrea", "surname": "leo" })
52
- .end((err, res) => {
53
-
54
- if (err) { console.error("err: ", err); }
55
- if (log) { console.log("res.body", res.body); }
56
-
57
- res.should.have.status(200);
58
- res.body.should.be.a('object');
59
- expect(res.body.token).to.not.equal(null);
60
-
61
- var jwtToken = res.body.token;
62
- if (log) { console.log("jwtToken", jwtToken); }
63
-
64
- chai.request(server)
65
- .post('/' + savedProject._id + '/jwt/decode')
66
- .set('Authorization', jwtToken)
67
- .send()
68
- .end((err, res) => {
69
-
70
- res.should.have.status(200);
71
- res.body.should.be.a('object');
72
-
73
- expect(res.body.name).to.not.equal("andrea");
74
- expect(res.body.surname).to.not.equal("leo");
75
- expect(res.body.iat).to.not.equal(null);
76
- expect(res.body.exp).to.not.equal(null);
77
-
78
- done();
79
- });
80
- });
39
+ expect(res.body.success).to.equal(false);
40
+ expect(res.body.msg).to.equal('deprecated');
41
+ done();
81
42
  });
82
43
  });
83
44
  });
84
45
  }).timeout(20000);
85
46
 
86
-
87
- it('decodeWithIatNoExp', (done) => {
88
-
47
+ it('decode returns deprecated', (done) => {
89
48
  var email = "test-signup-" + Date.now() + "@email.com";
90
49
  var pwd = "pwd";
91
50
 
92
51
  userService.signup(email, pwd, "Test Firstname", "Test lastname").then(function (savedUser) {
93
- projectService.create("test-join-member", savedUser._id).then(function (savedProject) {
52
+ projectService.create("test-jwt-deprecated-decode", savedUser._id).then(function (savedProject) {
94
53
  chai.request(server)
95
- .post('/' + savedProject._id + '/keys/generate')
54
+ .post('/' + savedProject._id + '/jwt/decode')
96
55
  .auth(email, pwd)
97
56
  .send()
98
57
  .end((err, res) => {
99
-
100
58
  if (err) { console.error("err: ", err); }
101
59
  if (log) { console.log("res.body", res.body); }
102
60
 
103
- res.should.have.status(200);
61
+ res.should.have.status(410);
104
62
  res.body.should.be.a('object');
105
- expect(res.body.jwtSecret).to.not.equal(null);
106
-
107
- var jwtToken = jwt.sign({ "name": "andrea", "surname": "leo" }, res.body.jwtSecret);
108
- if (log) { console.log("jwtToken", jwtToken); }
109
-
110
- chai.request(server)
111
- .post('/' + savedProject._id + '/jwt/decode')
112
- .set('Authorization', 'JWT ' + jwtToken)
113
- .send()
114
- .end((err, res) => {
115
-
116
- if (err) { console.error("err: ", err); }
117
- if (log) { console.log("res.body", res.body); }
118
-
119
- res.should.have.status(401);
120
-
121
- done();
122
- });
123
- });
124
- });
125
- });
126
- }).timeout(20000);
127
-
128
-
129
- it('decodeWithIatTooHightExp', (done) => {
130
-
131
- var email = "test-signup-" + Date.now() + "@email.com";
132
- var pwd = "pwd";
133
-
134
- userService.signup(email, pwd, "Test Firstname", "Test lastname").then(function (savedUser) {
135
- projectService.create("test-join-member", savedUser._id).then(function (savedProject) {
136
- chai.request(server)
137
- .post('/' + savedProject._id + '/keys/generate')
138
- .auth(email, pwd)
139
- .send()
140
- .end((err, res) => {
141
-
142
- if (err) { console.error("err: ", err); }
143
- if (log) { console.log("res.body", res.body); }
144
-
145
- res.should.have.status(200);
146
- res.body.should.be.a('object');
147
- expect(res.body.jwtSecret).to.not.equal(null);
148
-
149
- var jwtToken = jwt.sign({ "name": "andrea", "surname": "leo" }, res.body.jwtSecret, { expiresIn: 800 });
150
- if (log) { console.log("jwtToken", jwtToken); }
151
-
152
- chai.request(server)
153
- .post('/' + savedProject._id + '/jwt/decode')
154
- .set('Authorization', 'JWT ' + jwtToken)
155
- .send()
156
- .end((err, res) => {
157
-
158
- if (err) { console.error("err: ", err); }
159
- if (log) { console.log("res.body", res.body); }
160
-
161
- res.should.have.status(401);
162
-
163
- done();
164
- });
63
+ expect(res.body.success).to.equal(false);
64
+ expect(res.body.msg).to.equal('deprecated');
65
+ done();
165
66
  });
166
67
  });
167
68
  });
@@ -170,5 +71,3 @@ describe('JWTRoute', () => {
170
71
  });
171
72
 
172
73
  });
173
-
174
-