Skip to content

Commit 2cab3f0

Browse files
authored
misc: encode auth token in DB (#73)
1 parent 6e31207 commit 2cab3f0

8 files changed

+201
-13
lines changed

src/server/handlers/interactiveMessages.js

+9-7
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,24 @@ async function notificationInteractiveMessages(req, res) {
5858
res.send('ok');
5959
return;
6060
}
61-
if (!authToken || !authToken.data || authToken.data.length == 0) {
61+
const authTokenData = authToken && authToken.getDecryptedData();
62+
if (!authTokenData) {
6263
await sendAuthCardToRCWebhook(webhookRecord.rc_webhook, webhookId);
6364
res.status(200);
6465
res.send('ok');
6566
return;
6667
}
6768
try {
6869
const bugsnag = new Bugsnag({
69-
authToken: authToken.data,
70+
authToken: authTokenData,
7071
projectId: body.data.projectId,
7172
errorId: body.data.errorId,
7273
});
7374
await bugsnag.operate({ action, data: body.data });
7475
} catch (e) {
7576
if (e.response) {
7677
if (e.response.status === 401) {
77-
authToken.data = '';
78+
authToken.removeData();
7879
await authToken.save();
7980
await sendAuthCardToRCWebhook(webhookRecord.rc_webhook, webhookId);
8081
} else if (e.response.status === 403) {
@@ -220,7 +221,7 @@ async function botInteractiveMessagesHandler(req, res) {
220221
}
221222
if (action === 'removeAuthToken') {
222223
if (authToken) {
223-
authToken.data = '';
224+
authToken.removeData();
224225
await authToken.save();
225226
}
226227
const newCard = getAdaptiveCardFromTemplate(
@@ -239,7 +240,8 @@ async function botInteractiveMessagesHandler(req, res) {
239240
});
240241
return;
241242
}
242-
if (!authToken || !authToken.data || authToken.data.length == 0) {
243+
const authTokenData = authToken && authToken.getDecryptedData();
244+
if (!authTokenData) {
243245
await botActions.sendAuthCard(bot, groupId);
244246
res.status(200);
245247
res.send('ok');
@@ -252,7 +254,7 @@ async function botInteractiveMessagesHandler(req, res) {
252254
}
253255
try {
254256
const bugsnag = new Bugsnag({
255-
authToken: authToken.data,
257+
authToken: authTokenData,
256258
projectId: body.data.projectId,
257259
errorId: body.data.errorId,
258260
});
@@ -270,7 +272,7 @@ async function botInteractiveMessagesHandler(req, res) {
270272
let trackResult = 'error';
271273
if (e.response) {
272274
if (e.response.status === 401) {
273-
authToken.data = '';
275+
authToken.removeData();
274276
await authToken.save();
275277
await bot.sendAdaptiveCard(groupId, getAdaptiveCardFromTemplate(authTokenTemplate, {
276278
botId,

src/server/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const { extendApp: extendBotApp } = require('ringcentral-chatbot-core');
66

77
const notificationRoute = require('./routes/notification');
88
const subscriptionRoute = require('./routes/subscription');
9+
const maintainRoute = require('./routes/maintain');
10+
911
const { botHandler } = require('./bot/handler');
1012
const { botConfig } = require('./bot/config');
1113
const { errorLogger } = require('./utils/logger');
@@ -40,6 +42,7 @@ app.get('/webhook/new', subscriptionRoute.setup);
4042
app.post('/webhooks', refererChecker, subscriptionRoute.createWebhook);
4143

4244
app.post('/interactive-messages', notificationRoute.interactiveMessages);
45+
app.get('/maintain/migrate-encrypted-data', maintainRoute.migrateEncryptedData);
4346

4447
// bots:
4548
extendBotApp(app, [], botHandler, botConfig);

src/server/models/authToken.js

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,61 @@
1+
const crypto = require('crypto');
12
const Sequelize = require('sequelize');
23
const { sequelize } = require('./sequelize');
34

4-
exports.AuthToken = sequelize.define('authTokens', {
5+
const AuthToken = sequelize.define('authTokens', {
56
id: {
67
type: Sequelize.STRING,
78
primaryKey: true,
89
},
910
data: {
1011
type: Sequelize.STRING
1112
},
13+
encryptedData: {
14+
type: Sequelize.STRING
15+
},
1216
});
17+
18+
function getCipherKey() {
19+
if (!process.env.APP_SERVER_SECRET_KEY) {
20+
throw new Error('APP_SERVER_SECRET_KEY is not defined');
21+
}
22+
if (process.env.APP_SERVER_SECRET_KEY.length < 32) {
23+
// pad secret key with spaces if it is less than 32 bytes
24+
return process.env.APP_SERVER_SECRET_KEY.padEnd(32, ' ');
25+
}
26+
if (process.env.APP_SERVER_SECRET_KEY.length > 32) {
27+
// truncate secret key if it is more than 32 bytes
28+
return process.env.APP_SERVER_SECRET_KEY.slice(0, 32);
29+
}
30+
return process.env.APP_SERVER_SECRET_KEY;
31+
}
32+
33+
const originalSave = AuthToken.prototype.save;
34+
AuthToken.prototype.save = async function () {
35+
if (this.data) {
36+
// encode data to encryptedData
37+
const cipher = crypto
38+
.createCipheriv('aes-256-cbc', getCipherKey(), Buffer.alloc(16, 0))
39+
this.encryptedData = cipher.update(this.data, 'utf8', 'hex') + cipher.final('hex');
40+
this.data = '';
41+
}
42+
return originalSave.call(this);
43+
}
44+
45+
AuthToken.prototype.getDecryptedData = function () {
46+
if (!this.encryptedData) {
47+
// for backward compatibility
48+
return this.data;
49+
}
50+
// decode encryptedData to data
51+
const decipher = crypto
52+
.createDecipheriv('aes-256-cbc', getCipherKey(), Buffer.alloc(16, 0))
53+
return decipher.update(this.encryptedData, 'hex', 'utf8') + decipher.final('utf8');
54+
}
55+
56+
AuthToken.prototype.removeData = function () {
57+
this.data = '';
58+
this.encryptedData = '';
59+
}
60+
61+
exports.AuthToken = AuthToken;

src/server/routes/maintain.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const { AuthToken } = require('../models/authToken');
2+
const { errorLogger } = require('../utils/logger');
3+
4+
async function migrateEncryptedData(req, res) {
5+
if (!process.env.MAINTAIN_TOKEN) {
6+
res.status(404);
7+
res.send('Not found');
8+
return;
9+
}
10+
if (req.query.maintain_token !== process.env.MAINTAIN_TOKEN) {
11+
res.status(401);
12+
res.send('Token invalid');
13+
return;
14+
}
15+
try {
16+
const authTokens = await AuthToken.findAll();
17+
for (const authToken of authTokens) {
18+
if (authToken.data) {
19+
await authToken.save();
20+
}
21+
}
22+
res.status(200);
23+
res.send('migrated');
24+
} catch (e) {
25+
errorLogger(e);
26+
res.status(500);
27+
res.send('internal error');
28+
}
29+
}
30+
31+
exports.migrateEncryptedData = migrateEncryptedData;

tests/authToken.test.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const { AuthToken } = require('../src/server/models/authToken');
2+
3+
describe('authToken', () => {
4+
it('should create auth token with encrypted data', async () => {
5+
const authToken = await AuthToken.create({
6+
id: '123',
7+
data: 'test',
8+
});
9+
expect(authToken.encryptedData).not.toBe('');
10+
expect(authToken.data).toBe('');
11+
12+
const savedAuthToken = await AuthToken.findByPk('123');
13+
expect(savedAuthToken.data).toBe('');
14+
expect(savedAuthToken.getDecryptedData()).toBe('test');
15+
expect(savedAuthToken.encryptedData).not.toBe('');
16+
await savedAuthToken.destroy();
17+
});
18+
19+
it('should create auth token without data', async () => {
20+
const authToken = await AuthToken.create({
21+
id: '123',
22+
});
23+
expect(authToken.encryptedData).toBe(undefined);
24+
expect(authToken.data).toBe(undefined);
25+
26+
const savedAuthToken = await AuthToken.findByPk('123');
27+
expect(savedAuthToken.data).toBe(null);
28+
expect(savedAuthToken.getDecryptedData()).toBe(null);
29+
expect(savedAuthToken.encryptedData).toBe(null);
30+
await savedAuthToken.destroy();
31+
});
32+
33+
it('should get decoded data successfully', async () => {
34+
const authToken = await AuthToken.create({
35+
id: '123',
36+
encryptedData: 'cfe2148b1b5236137f58348954930ba6',
37+
});
38+
expect(authToken.getDecryptedData()).toBe('test');
39+
await authToken.destroy();
40+
});
41+
42+
it('should remove auth token data successfully', async () => {
43+
const authToken = await AuthToken.create({
44+
id: '123',
45+
data: 'test',
46+
});
47+
authToken.removeData();
48+
await authToken.save();
49+
50+
const savedAuthToken = await AuthToken.findByPk('123');
51+
expect(savedAuthToken.data).toBe('');
52+
expect(savedAuthToken.encryptedData).toBe('');
53+
expect(savedAuthToken.getDecryptedData()).toBe('');
54+
await savedAuthToken.destroy();
55+
});
56+
});

tests/bot-interactive-messages.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ describe('Bot', () => {
132132
expect(res.status).toEqual(200);
133133
expect(requestBody.type).toContain('AdaptiveCard');
134134
expect(JSON.stringify(requestBody.body)).toContain('token is saved successfully');
135-
expect(authToken.data).toEqual('test-token');
135+
expect(authToken.getDecryptedData()).toEqual('test-token');
136136
rcCardScope.done();
137137
});
138138

@@ -168,7 +168,7 @@ describe('Bot', () => {
168168
expect(res.status).toEqual(200);
169169
expect(requestBody.type).toContain('AdaptiveCard');
170170
expect(JSON.stringify(requestBody.body)).toContain('token is removed successfully');
171-
expect(authToken.data).toEqual('');
171+
expect(authToken.getDecryptedData()).toEqual('');
172172
rcCardScope.done();
173173
});
174174

tests/maintain.test.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const request = require('supertest');
2+
const { server } = require('../src/server');
3+
const { AuthToken } = require('../src/server/models/authToken');
4+
describe('Maintain', () => {
5+
it('should return 404 if MAINTAIN_TOKEN is not set', async () => {
6+
const res = await request(server).get('/maintain/migrate-encrypted-data');
7+
expect(res.status).toBe(404);
8+
expect(res.text).toBe('Not found');
9+
});
10+
11+
it('should return 401 if maintain_token is invalid', async () => {
12+
process.env.MAINTAIN_TOKEN = 'maintain_token_xxx';
13+
const res = await request(server).get('/maintain/migrate-encrypted-data?maintain_token=invalid');
14+
expect(res.status).toBe(401);
15+
expect(res.text).toBe('Token invalid');
16+
});
17+
18+
it('should return 200 if maintain_token is valid', async () => {
19+
process.env.MAINTAIN_TOKEN = 'maintain_token_xxx';
20+
const res = await request(server).get(`/maintain/migrate-encrypted-data?maintain_token=${process.env.MAINTAIN_TOKEN}`);
21+
expect(res.status).toBe(200);
22+
expect(res.text).toBe('migrated');
23+
});
24+
25+
it('should migrate encrypted data', async () => {
26+
await AuthToken.create({
27+
id: '1111',
28+
data: 'test1',
29+
});
30+
await AuthToken.create({
31+
id: '2222',
32+
});
33+
const authToken = await AuthToken.findByPk('2222');
34+
await authToken.update({
35+
data: 'test',
36+
});
37+
await request(server).get(`/maintain/migrate-encrypted-data?maintain_token=${process.env.MAINTAIN_TOKEN}`);
38+
const authToken1 = await AuthToken.findByPk('1111');
39+
const authToken2 = await AuthToken.findByPk('2222');
40+
expect(authToken1.data).toBe('');
41+
expect(authToken1.getDecryptedData()).toBe('test1');
42+
expect(authToken2.data).toBe('');
43+
expect(authToken2.getDecryptedData()).toBe('test');
44+
await authToken1.destroy();
45+
await authToken2.destroy();
46+
});
47+
});

tests/notification-interactive-messages.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ describe('Notification Interactive Messages', () => {
132132
expect(res.status).toEqual(200);
133133
expect(requestBody.title).toContain('token is saved');
134134
const authToken = await AuthToken.findByPk('test-account-id-test-user-id');
135-
expect(authToken.data).toEqual('test-token');
135+
expect(authToken.getDecryptedData()).toEqual('test-token');
136136
scope.done();
137137
});
138138

@@ -145,7 +145,7 @@ describe('Notification Interactive Messages', () => {
145145
requestBody = JSON.parse(reqBody);
146146
});
147147
let authToken = await AuthToken.findByPk('test-account-id-test-user-id');
148-
expect(!!authToken.data).toEqual(true);
148+
expect(!!authToken.getDecryptedData()).toEqual(true);
149149
const res = await request(server).post('/interactive-messages').send({
150150
data: {
151151
webhookId: webhookRecord.id,
@@ -163,7 +163,7 @@ describe('Notification Interactive Messages', () => {
163163
expect(res.status).toEqual(200);
164164
expect(requestBody.title).toContain('token is saved');
165165
authToken = await AuthToken.findByPk('test-account-id-test-user-id');
166-
expect(authToken.data).toEqual('test-token-2');
166+
expect(authToken.getDecryptedData()).toEqual('test-token-2');
167167
scope.done();
168168
});
169169

0 commit comments

Comments
 (0)