Skip to content

Commit

Permalink
misc: encode auth token in DB (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
embbnux authored Jul 18, 2024
1 parent 6e31207 commit 2cab3f0
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 13 deletions.
16 changes: 9 additions & 7 deletions src/server/handlers/interactiveMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,24 @@ async function notificationInteractiveMessages(req, res) {
res.send('ok');
return;
}
if (!authToken || !authToken.data || authToken.data.length == 0) {
const authTokenData = authToken && authToken.getDecryptedData();
if (!authTokenData) {
await sendAuthCardToRCWebhook(webhookRecord.rc_webhook, webhookId);
res.status(200);
res.send('ok');
return;
}
try {
const bugsnag = new Bugsnag({
authToken: authToken.data,
authToken: authTokenData,
projectId: body.data.projectId,
errorId: body.data.errorId,
});
await bugsnag.operate({ action, data: body.data });
} catch (e) {
if (e.response) {
if (e.response.status === 401) {
authToken.data = '';
authToken.removeData();
await authToken.save();
await sendAuthCardToRCWebhook(webhookRecord.rc_webhook, webhookId);
} else if (e.response.status === 403) {
Expand Down Expand Up @@ -220,7 +221,7 @@ async function botInteractiveMessagesHandler(req, res) {
}
if (action === 'removeAuthToken') {
if (authToken) {
authToken.data = '';
authToken.removeData();
await authToken.save();
}
const newCard = getAdaptiveCardFromTemplate(
Expand All @@ -239,7 +240,8 @@ async function botInteractiveMessagesHandler(req, res) {
});
return;
}
if (!authToken || !authToken.data || authToken.data.length == 0) {
const authTokenData = authToken && authToken.getDecryptedData();
if (!authTokenData) {
await botActions.sendAuthCard(bot, groupId);
res.status(200);
res.send('ok');
Expand All @@ -252,7 +254,7 @@ async function botInteractiveMessagesHandler(req, res) {
}
try {
const bugsnag = new Bugsnag({
authToken: authToken.data,
authToken: authTokenData,
projectId: body.data.projectId,
errorId: body.data.errorId,
});
Expand All @@ -270,7 +272,7 @@ async function botInteractiveMessagesHandler(req, res) {
let trackResult = 'error';
if (e.response) {
if (e.response.status === 401) {
authToken.data = '';
authToken.removeData();
await authToken.save();
await bot.sendAdaptiveCard(groupId, getAdaptiveCardFromTemplate(authTokenTemplate, {
botId,
Expand Down
3 changes: 3 additions & 0 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const { extendApp: extendBotApp } = require('ringcentral-chatbot-core');

const notificationRoute = require('./routes/notification');
const subscriptionRoute = require('./routes/subscription');
const maintainRoute = require('./routes/maintain');

const { botHandler } = require('./bot/handler');
const { botConfig } = require('./bot/config');
const { errorLogger } = require('./utils/logger');
Expand Down Expand Up @@ -40,6 +42,7 @@ app.get('/webhook/new', subscriptionRoute.setup);
app.post('/webhooks', refererChecker, subscriptionRoute.createWebhook);

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

// bots:
extendBotApp(app, [], botHandler, botConfig);
Expand Down
51 changes: 50 additions & 1 deletion src/server/models/authToken.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,61 @@
const crypto = require('crypto');
const Sequelize = require('sequelize');
const { sequelize } = require('./sequelize');

exports.AuthToken = sequelize.define('authTokens', {
const AuthToken = sequelize.define('authTokens', {
id: {
type: Sequelize.STRING,
primaryKey: true,
},
data: {
type: Sequelize.STRING
},
encryptedData: {
type: Sequelize.STRING
},
});

function getCipherKey() {
if (!process.env.APP_SERVER_SECRET_KEY) {
throw new Error('APP_SERVER_SECRET_KEY is not defined');
}
if (process.env.APP_SERVER_SECRET_KEY.length < 32) {
// pad secret key with spaces if it is less than 32 bytes
return process.env.APP_SERVER_SECRET_KEY.padEnd(32, ' ');
}
if (process.env.APP_SERVER_SECRET_KEY.length > 32) {
// truncate secret key if it is more than 32 bytes
return process.env.APP_SERVER_SECRET_KEY.slice(0, 32);
}
return process.env.APP_SERVER_SECRET_KEY;
}

const originalSave = AuthToken.prototype.save;
AuthToken.prototype.save = async function () {
if (this.data) {
// encode data to encryptedData
const cipher = crypto
.createCipheriv('aes-256-cbc', getCipherKey(), Buffer.alloc(16, 0))
this.encryptedData = cipher.update(this.data, 'utf8', 'hex') + cipher.final('hex');
this.data = '';
}
return originalSave.call(this);
}

AuthToken.prototype.getDecryptedData = function () {
if (!this.encryptedData) {
// for backward compatibility
return this.data;
}
// decode encryptedData to data
const decipher = crypto
.createDecipheriv('aes-256-cbc', getCipherKey(), Buffer.alloc(16, 0))
return decipher.update(this.encryptedData, 'hex', 'utf8') + decipher.final('utf8');
}

AuthToken.prototype.removeData = function () {
this.data = '';
this.encryptedData = '';
}

exports.AuthToken = AuthToken;
31 changes: 31 additions & 0 deletions src/server/routes/maintain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { AuthToken } = require('../models/authToken');
const { errorLogger } = require('../utils/logger');

async function migrateEncryptedData(req, res) {
if (!process.env.MAINTAIN_TOKEN) {
res.status(404);
res.send('Not found');
return;
}
if (req.query.maintain_token !== process.env.MAINTAIN_TOKEN) {
res.status(401);
res.send('Token invalid');
return;
}
try {
const authTokens = await AuthToken.findAll();
for (const authToken of authTokens) {
if (authToken.data) {
await authToken.save();
}
}
res.status(200);
res.send('migrated');
} catch (e) {
errorLogger(e);
res.status(500);
res.send('internal error');
}
}

exports.migrateEncryptedData = migrateEncryptedData;
56 changes: 56 additions & 0 deletions tests/authToken.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const { AuthToken } = require('../src/server/models/authToken');

describe('authToken', () => {
it('should create auth token with encrypted data', async () => {
const authToken = await AuthToken.create({
id: '123',
data: 'test',
});
expect(authToken.encryptedData).not.toBe('');
expect(authToken.data).toBe('');

const savedAuthToken = await AuthToken.findByPk('123');
expect(savedAuthToken.data).toBe('');
expect(savedAuthToken.getDecryptedData()).toBe('test');
expect(savedAuthToken.encryptedData).not.toBe('');
await savedAuthToken.destroy();
});

it('should create auth token without data', async () => {
const authToken = await AuthToken.create({
id: '123',
});
expect(authToken.encryptedData).toBe(undefined);
expect(authToken.data).toBe(undefined);

const savedAuthToken = await AuthToken.findByPk('123');
expect(savedAuthToken.data).toBe(null);
expect(savedAuthToken.getDecryptedData()).toBe(null);
expect(savedAuthToken.encryptedData).toBe(null);
await savedAuthToken.destroy();
});

it('should get decoded data successfully', async () => {
const authToken = await AuthToken.create({
id: '123',
encryptedData: 'cfe2148b1b5236137f58348954930ba6',
});
expect(authToken.getDecryptedData()).toBe('test');
await authToken.destroy();
});

it('should remove auth token data successfully', async () => {
const authToken = await AuthToken.create({
id: '123',
data: 'test',
});
authToken.removeData();
await authToken.save();

const savedAuthToken = await AuthToken.findByPk('123');
expect(savedAuthToken.data).toBe('');
expect(savedAuthToken.encryptedData).toBe('');
expect(savedAuthToken.getDecryptedData()).toBe('');
await savedAuthToken.destroy();
});
});
4 changes: 2 additions & 2 deletions tests/bot-interactive-messages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ describe('Bot', () => {
expect(res.status).toEqual(200);
expect(requestBody.type).toContain('AdaptiveCard');
expect(JSON.stringify(requestBody.body)).toContain('token is saved successfully');
expect(authToken.data).toEqual('test-token');
expect(authToken.getDecryptedData()).toEqual('test-token');
rcCardScope.done();
});

Expand Down Expand Up @@ -168,7 +168,7 @@ describe('Bot', () => {
expect(res.status).toEqual(200);
expect(requestBody.type).toContain('AdaptiveCard');
expect(JSON.stringify(requestBody.body)).toContain('token is removed successfully');
expect(authToken.data).toEqual('');
expect(authToken.getDecryptedData()).toEqual('');
rcCardScope.done();
});

Expand Down
47 changes: 47 additions & 0 deletions tests/maintain.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const request = require('supertest');
const { server } = require('../src/server');
const { AuthToken } = require('../src/server/models/authToken');
describe('Maintain', () => {
it('should return 404 if MAINTAIN_TOKEN is not set', async () => {
const res = await request(server).get('/maintain/migrate-encrypted-data');
expect(res.status).toBe(404);
expect(res.text).toBe('Not found');
});

it('should return 401 if maintain_token is invalid', async () => {
process.env.MAINTAIN_TOKEN = 'maintain_token_xxx';
const res = await request(server).get('/maintain/migrate-encrypted-data?maintain_token=invalid');
expect(res.status).toBe(401);
expect(res.text).toBe('Token invalid');
});

it('should return 200 if maintain_token is valid', async () => {
process.env.MAINTAIN_TOKEN = 'maintain_token_xxx';
const res = await request(server).get(`/maintain/migrate-encrypted-data?maintain_token=${process.env.MAINTAIN_TOKEN}`);
expect(res.status).toBe(200);
expect(res.text).toBe('migrated');
});

it('should migrate encrypted data', async () => {
await AuthToken.create({
id: '1111',
data: 'test1',
});
await AuthToken.create({
id: '2222',
});
const authToken = await AuthToken.findByPk('2222');
await authToken.update({
data: 'test',
});
await request(server).get(`/maintain/migrate-encrypted-data?maintain_token=${process.env.MAINTAIN_TOKEN}`);
const authToken1 = await AuthToken.findByPk('1111');
const authToken2 = await AuthToken.findByPk('2222');
expect(authToken1.data).toBe('');
expect(authToken1.getDecryptedData()).toBe('test1');
expect(authToken2.data).toBe('');
expect(authToken2.getDecryptedData()).toBe('test');
await authToken1.destroy();
await authToken2.destroy();
});
});
6 changes: 3 additions & 3 deletions tests/notification-interactive-messages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ describe('Notification Interactive Messages', () => {
expect(res.status).toEqual(200);
expect(requestBody.title).toContain('token is saved');
const authToken = await AuthToken.findByPk('test-account-id-test-user-id');
expect(authToken.data).toEqual('test-token');
expect(authToken.getDecryptedData()).toEqual('test-token');
scope.done();
});

Expand All @@ -145,7 +145,7 @@ describe('Notification Interactive Messages', () => {
requestBody = JSON.parse(reqBody);
});
let authToken = await AuthToken.findByPk('test-account-id-test-user-id');
expect(!!authToken.data).toEqual(true);
expect(!!authToken.getDecryptedData()).toEqual(true);
const res = await request(server).post('/interactive-messages').send({
data: {
webhookId: webhookRecord.id,
Expand All @@ -163,7 +163,7 @@ describe('Notification Interactive Messages', () => {
expect(res.status).toEqual(200);
expect(requestBody.title).toContain('token is saved');
authToken = await AuthToken.findByPk('test-account-id-test-user-id');
expect(authToken.data).toEqual('test-token-2');
expect(authToken.getDecryptedData()).toEqual('test-token-2');
scope.done();
});

Expand Down

0 comments on commit 2cab3f0

Please sign in to comment.