Skip to content

Commit

Permalink
Regression: Encode registration info as JWT when signing key is provi…
Browse files Browse the repository at this point in the history
…ded (#24626)

* Encode credentials as JWT when signing key is provided

* Endpoint protection
  • Loading branch information
KevLehman authored Feb 25, 2022
1 parent b441259 commit 43fa95a
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 17 deletions.
39 changes: 29 additions & 10 deletions app/api/server/v1/voip/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Match, check } from 'meteor/check';

import { API } from '../../api';
import { hasPermission } from '../../../../authorization/server/index';
import { Users } from '../../../../models/server/raw/index';
import { Voip } from '../../../../../server/sdk';
import { IVoipExtensionBase } from '../../../../../definition/IVoipExtension';
import { generateJWT } from '../../../../utils/server/lib/JWTHelper';
import { settings } from '../../../../settings/server';
import { logger } from './logger';

// Get the connector version and type
API.v1.addRoute(
'connector.getVersion',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['manage-voip-call-settings'] },
{
async get() {
const version = await Voip.getConnectorVersion();
Expand All @@ -21,7 +23,7 @@ API.v1.addRoute(
// Get the extensions available on the call server
API.v1.addRoute(
'connector.extension.list',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['manage-voip-call-settings'] },
{
async get() {
const list = await Voip.getExtensionList();
Expand All @@ -37,7 +39,7 @@ API.v1.addRoute(
*/
API.v1.addRoute(
'connector.extension.getDetails',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['manage-voip-call-settings'] },
{
async get() {
check(
Expand All @@ -57,7 +59,7 @@ API.v1.addRoute(

API.v1.addRoute(
'connector.extension.getRegistrationInfoByExtension',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['manage-voip-call-settings'] },
{
async get() {
check(
Expand All @@ -67,14 +69,21 @@ API.v1.addRoute(
}),
);
const endpointDetails = await Voip.getRegistrationInfo(this.requestParams());
return API.v1.success({ ...endpointDetails.result });
const encKey = settings.get('VoIP_JWT_Secret');
if (!encKey) {
logger.warn('No JWT keys set. Sending registration info as plain text');
return API.v1.success({ ...endpointDetails.result });
}

const result = generateJWT(endpointDetails.result, encKey);
return API.v1.success({ result });
},
},
);

API.v1.addRoute(
'connector.extension.getRegistrationInfoByUserId',
{ authRequired: true },
{ authRequired: true, permissionsRequired: ['view-agent-extension-association'] },
{
async get() {
check(
Expand All @@ -83,10 +92,12 @@ API.v1.addRoute(
id: String,
}),
);
if (!hasPermission(this.userId, 'view-agent-extension-association')) {
const { id } = this.requestParams();

if (id !== this.userId) {
return API.v1.unauthorized();
}
const { id } = this.requestParams();

const { extension } =
(await Users.getVoipExtensionByUserId(id, {
projection: {
Expand All @@ -99,8 +110,16 @@ API.v1.addRoute(
if (!extension) {
return API.v1.notFound('Extension not found');
}

const endpointDetails = await Voip.getRegistrationInfo({ extension });
return API.v1.success({ ...endpointDetails.result });
const encKey = settings.get('VoIP_JWT_Secret');
if (!encKey) {
logger.warn('No JWT keys set. Sending registration info as plain text');
return API.v1.success({ ...endpointDetails.result });
}

const result = generateJWT(endpointDetails.result, encKey);
return API.v1.success({ result });
},
},
);
11 changes: 9 additions & 2 deletions app/lib/server/startup/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3150,10 +3150,17 @@ settingsRegistry.addGroup('Call_Center', function () {
value: true,
},
});

this.add('VoIP_JWT_Secret', '', {
type: 'password',
i18nDescription: 'VoIP_JWT_Secret_description',
enableQuery: {
_id: 'VoIP_Enabled',
value: true,
},
});
this.section('Server_Configuration', function () {
this.add('VoIP_Server_Host', '', {
type: 'string',
type: 'password',
public: true,
enableQuery: {
_id: 'VoIP_Enabled',
Expand Down
16 changes: 14 additions & 2 deletions client/providers/CallProvider/hooks/useVoipClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useSafely } from '@rocket.chat/fuselage-hooks';
import { KJUR } from 'jsrsasign';
import { useEffect, useState } from 'react';

import { IRegistrationInfo } from '../../../../definition/voip/IRegistrationInfo';
Expand All @@ -23,6 +24,8 @@ export const isUseVoipClientResultError = (result: UseVoipClientResult): result
export const isUseVoipClientResultLoading = (result: UseVoipClientResult): result is UseVoipClientResultLoading =>
!result || !Object.keys(result).length;

const isSignedResponse = (data: any): data is { result: string } => typeof data?.result === 'string';

export const useVoipClient = (): UseVoipClientResult => {
const registrationInfo = useEndpoint('GET', 'connector.extension.getRegistrationInfoByUserId');
const user = useUser();
Expand All @@ -37,16 +40,25 @@ export const useVoipClient = (): UseVoipClientResult => {
}
registrationInfo({ id: user._id }).then(
(data) => {
let parsedData: IRegistrationInfo;
if (isSignedResponse(data)) {
const result = KJUR.jws.JWS.parse(data.result);
parsedData = (result.payloadObj as any)?.context as IRegistrationInfo;
} else {
parsedData = data;
}

const {
extensionDetails: { extension, password },
host,
callServerConfig: { websocketPath },
} = data;
} = parsedData;

let client: VoIPUser;
(async (): Promise<void> => {
try {
client = await SimpleVoipUser.create(extension, password, host, websocketPath, iceServers, 'video');
setResult({ voipClient: client, registrationInfo: data });
setResult({ voipClient: client, registrationInfo: parsedData });
} catch (e) {
setResult({ error: e as Error });
}
Expand Down
2 changes: 1 addition & 1 deletion definition/rest/v1/voip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PaginatedResult } from '../helpers/PaginatedResult';

export type VoipEndpoints = {
'connector.extension.getRegistrationInfoByUserId': {
GET: (params: { id: string }) => IRegistrationInfo;
GET: (params: { id: string }) => IRegistrationInfo | { result: string };
};
'voip/queues.getSummary': {
GET: () => { summary: IQueueSummary[] };
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@
"@types/dompurify": "^2.2.2",
"@types/ejson": "^2.1.3",
"@types/express": "^4.17.12",
"@types/google-libphonenumber": "^7.4.21",
"@types/fibers": "^3.1.1",
"@types/google-libphonenumber": "^7.4.21",
"@types/imap": "^0.8.35",
"@types/jsdom": "^16.2.12",
"@types/jsdom-global": "^3.0.2",
"@types/jsrsasign": "^9.0.1",
"@types/jsrsasign": "^9.0.3",
"@types/ldapjs": "^2.2.1",
"@types/less": "^3.0.2",
"@types/lodash.get": "^4.4.6",
Expand Down
2 changes: 2 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -4737,6 +4737,8 @@
"Voip_call_ended": "Call ended at",
"Voip_call_ended_unexpectedly": "Call ended unexpectedly: __reason__",
"Voip_call_wrapup": "Call wrapup notes added: __comment__",
"VoIP_JWT_Secret": "VoIP JWT Secret",
"VoIP_JWT_Secret_description": "This allows you to set a secret key for sharing extension details from server to client as JWT instead of plain text. If you don't setup this, extension registration details will be sent as plain text",
"Chat_opened_by_visitor": "Chat opened by the visitor",
"Wait_activation_warning": "Before you can login, your account must be manually activated by an administrator.",
"Waiting_queue": "Waiting queue",
Expand Down

0 comments on commit 43fa95a

Please sign in to comment.