diff --git a/app/videobridge/client/views/videoFlexTab.js b/app/videobridge/client/views/videoFlexTab.js index 290558e1240f..6aedc954a968 100644 --- a/app/videobridge/client/views/videoFlexTab.js +++ b/app/videobridge/client/views/videoFlexTab.js @@ -86,7 +86,7 @@ Template.videoFlexTab.onRendered(function() { return closePanel(); } this.intervalHandler = null; - this.autorun(() => { + this.autorun(async () => { if (!settings.get('Jitsi_Enabled')) { return closePanel(); } @@ -99,6 +99,7 @@ Template.videoFlexTab.onRendered(function() { const domain = settings.get('Jitsi_Domain'); const jitsiRoom = settings.get('Jitsi_URL_Room_Prefix') + settings.get('uniqueID') + rid; const noSsl = !settings.get('Jitsi_SSL'); + const isEnabledTokenAuth = settings.get('Jitsi_Enabled_TokenAuth'); if (jitsiRoomActive !== null && jitsiRoomActive !== jitsiRoom) { jitsiRoomActive = null; @@ -108,11 +109,28 @@ Template.videoFlexTab.onRendered(function() { return stop(); } + let accessToken = null; + if (isEnabledTokenAuth) { + accessToken = await new Promise((resolve, reject) => { + Meteor.call('jitsi:generateAccessToken', rid, (error, result) => { + if (error) { + return reject(error); + } + resolve(result); + }); + }); + } + jitsiRoomActive = jitsiRoom; if (settings.get('Jitsi_Open_New_Window')) { start(); - const newWindow = window.open(`${ (noSsl ? 'http://' : 'https://') + domain }/${ jitsiRoom }`, jitsiRoom); + let queryString = ''; + if (accessToken) { + queryString = `?jwt=${ accessToken }`; + } + + const newWindow = window.open(`${ (noSsl ? 'http://' : 'https://') + domain }/${ jitsiRoom }${ queryString }`, jitsiRoom); if (newWindow) { const closeInterval = setInterval(() => { if (newWindow.closed === false) { @@ -130,7 +148,7 @@ Template.videoFlexTab.onRendered(function() { // Keep it from showing duplicates when re-evaluated on variable change. const name = Users.findOne(Meteor.userId(), { fields: { name: 1 } }); if (!$('[id^=jitsiConference]').length) { - this.api = new JitsiMeetExternalAPI(domain, jitsiRoom, width, height, this.$('.video-container').get(0), configOverwrite, interfaceConfigOverwrite, noSsl); + this.api = new JitsiMeetExternalAPI(domain, jitsiRoom, width, height, this.$('.video-container').get(0), configOverwrite, interfaceConfigOverwrite, noSsl, accessToken); /* * Hack to send after frame is loaded. diff --git a/app/videobridge/server/index.js b/app/videobridge/server/index.js index e3a113b21d0d..ee24f5157b78 100644 --- a/app/videobridge/server/index.js +++ b/app/videobridge/server/index.js @@ -1,5 +1,6 @@ import '../lib/messageType'; import './settings'; import './methods/jitsiSetTimeout'; +import './methods/jitsiGenerateToken'; import './methods/bbb'; import './actionLink'; diff --git a/app/videobridge/server/methods/jitsiGenerateToken.js b/app/videobridge/server/methods/jitsiGenerateToken.js new file mode 100644 index 000000000000..e84808ef15d1 --- /dev/null +++ b/app/videobridge/server/methods/jitsiGenerateToken.js @@ -0,0 +1,69 @@ +import { Meteor } from 'meteor/meteor'; +import { jws } from 'jsrsasign'; + +import { Rooms } from '../../../models'; +import { settings } from '../../../settings'; +import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; + +Meteor.methods({ + 'jitsi:generateAccessToken': (rid) => { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'jitsi:generateToken' }); + } + + const room = Rooms.findOneById(rid); + + if (!canAccessRoom(room, Meteor.user())) { + throw new Meteor.Error('error-not-allowed', 'not allowed', { method: 'jitsi:generateToken' }); + } + + const jitsiRoom = settings.get('Jitsi_URL_Room_Prefix') + settings.get('uniqueID') + rid; + + const jitsiDomain = settings.get('Jitsi_Domain'); + const jitsiApplicationId = settings.get('Jitsi_Application_ID'); + const jitsiApplicationSecret = settings.get('Jitsi_Application_Secret'); + const jitsiLimitTokenToRoom = settings.get('Jitsi_Limit_Token_To_Room'); + + function addUserContextToPayload(payload) { + const user = Meteor.user(); + payload.context = { + user: { + name: user.name, + email: user.emails[0].address, + avatar: Meteor.absoluteUrl(`avatar/${ user.username }`), + id: user._id, + }, + }; + + return payload; + } + + const JITSI_OPTIONS = { + jitsi_domain: jitsiDomain, + jitsi_lifetime_token: '1hour', // only 1 hour (for security reasons) + jitsi_application_id: jitsiApplicationId, + jitsi_application_secret: jitsiApplicationSecret, + }; + + const HEADER = { + typ: 'JWT', + alg: 'HS256', + }; + + const commonPayload = { + iss: JITSI_OPTIONS.jitsi_application_id, + sub: JITSI_OPTIONS.jitsi_domain, + iat: jws.IntDate.get('now'), + nbf: jws.IntDate.get('now'), + exp: jws.IntDate.get(`now + ${ JITSI_OPTIONS.jitsi_lifetime_token }`), + aud: 'RocketChat', + room: jitsiLimitTokenToRoom ? jitsiRoom : '*', + context: '', // first empty + }; + + const header = JSON.stringify(HEADER); + const payload = JSON.stringify(addUserContextToPayload(commonPayload)); + + return jws.JWS.sign(HEADER.alg, header, payload, { rstr: JITSI_OPTIONS.jitsi_application_secret }); + }, +}); diff --git a/app/videobridge/server/settings.js b/app/videobridge/server/settings.js index cfb7f90e8d53..38e2ca4e5d80 100644 --- a/app/videobridge/server/settings.js +++ b/app/videobridge/server/settings.js @@ -128,6 +128,44 @@ Meteor.startup(function() { i18nLabel: 'Jitsi_Chrome_Extension', public: true, }); + + this.add('Jitsi_Enabled_TokenAuth', false, { + type: 'boolean', + enableQuery: { + _id: 'Jitsi_Enabled', + value: true, + }, + i18nLabel: 'Jitsi_Enabled_TokenAuth', + public: true, + }); + + this.add('Jitsi_Application_ID', '', { + type: 'string', + enableQuery: [ + { _id: 'Jitsi_Enabled', value: true }, + { _id: 'Jitsi_Enabled_TokenAuth', value: true }, + ], + i18nLabel: 'Jitsi_Application_ID', + }); + + this.add('Jitsi_Application_Secret', '', { + type: 'string', + enableQuery: [ + { _id: 'Jitsi_Enabled', value: true }, + { _id: 'Jitsi_Enabled_TokenAuth', value: true }, + ], + i18nLabel: 'Jitsi_Application_Secret', + }); + + this.add('Jitsi_Limit_Token_To_Room', true, { + type: 'boolean', + enableQuery: [ + { _id: 'Jitsi_Enabled', value: true }, + { _id: 'Jitsi_Enabled_TokenAuth', value: true }, + ], + i18nLabel: 'Jitsi_Limit_Token_To_Room', + public: true, + }); }); }); }); diff --git a/package-lock.json b/package-lock.json index e7e3ec3023c5..431d8c863dde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10824,6 +10824,11 @@ "verror": "1.10.0" } }, + "jsrsasign": { + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-8.0.12.tgz", + "integrity": "sha1-Iqu5ZW00owuVMENnIINeicLlwxY=" + }, "juice": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/juice/-/juice-5.2.0.tgz", diff --git a/package.json b/package.json index 46a16ad43e6b..a6574c3d2c2f 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,7 @@ "ip-range-check": "^0.0.2", "jquery": "^3.3.1", "jschardet": "^1.6.0", + "jsrsasign": "^8.0.12", "juice": "^5.2.0", "katex": "^0.9.0", "ldap-escape": "^2.0.1", diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index ff85ebe35030..9766ed2e7116 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -1696,6 +1696,9 @@ "italic": "Kursiv", "italics": "kursiv", "Jitsi_Chrome_Extension": "Chrome Extension ID", + "Jitsi_Enabled_TokenAuth": "Einschalten JWT genehmigung", + "Jitsi_Application_ID": "Anwendungs ID (iss)", + "Jitsi_Application_Secret": "Anwendungsgeheimnis", "Jitsi_Enable_Channels": "In Kanälen aktivieren", "Job_Title": "Berufsbezeichnung", "join": "Beitreten", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index f5b071ac8c3b..90085e1516a8 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1723,7 +1723,11 @@ "italic": "Italic", "italics": "italics", "Jitsi_Chrome_Extension": "Chrome Extension Id", + "Jitsi_Enabled_TokenAuth": "Enable JWT auth", + "Jitsi_Application_ID": "Application ID (iss)", + "Jitsi_Application_Secret": "Application Secret", "Jitsi_Enable_Channels": "Enable in Channels", + "Jitsi_Limit_Token_To_Room": "Limit token to Jitsi Room", "Job_Title": "Job Title", "join": "Join", "join-without-join-code": "Join Without Join Code", diff --git a/packages/rocketchat-i18n/i18n/ru.i18n.json b/packages/rocketchat-i18n/i18n/ru.i18n.json index 39f5f1377b3e..f31b9c183271 100644 --- a/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -1723,6 +1723,9 @@ "italic": "Курсивный", "italics": "курсив", "Jitsi_Chrome_Extension": "Идентификатор расширения для Chrome ", + "Jitsi_Enabled_TokenAuth": "Включить JWT авторизацию", + "Jitsi_Application_ID": "Идентификатор приложения (iss)", + "Jitsi_Application_Secret": "Секретный ключ", "Jitsi_Enable_Channels": "Включить на канале", "Job_Title": "Должность", "join": "Присоединиться", diff --git a/public/packages/rocketchat_videobridge/client/public/external_api.js b/public/packages/rocketchat_videobridge/client/public/external_api.js index 50825e32b2cc..fc5e3d7af595 100644 --- a/public/packages/rocketchat_videobridge/client/public/external_api.js +++ b/public/packages/rocketchat_videobridge/client/public/external_api.js @@ -93,9 +93,10 @@ var JitsiMeetExternalAPI; * @param filmStripOnly if the value is true only the small videos will be * visible. * @param noSsl if the value is true https won't be used + * @param token if you need token authentication, then pass the token * @constructor */ -function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode, configOverwrite, interfaceConfigOverwrite, noSsl) { +function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode, configOverwrite, interfaceConfigOverwrite, noSsl, token) { if (!width || width < MIN_WIDTH) width = MIN_WIDTH; if (!height || height < MIN_HEIGHT) height = MIN_HEIGHT; @@ -114,6 +115,9 @@ function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode, conf this.frameName = "jitsiConferenceFrame" + id; this.url = (noSsl ? "http" : "https") + "://" + domain + "/"; if (room_name) this.url += room_name; + if (token) { + this.url += "?jwt=" + token; + } this.url += "#jitsi_meet_external_api_id=" + id; var key;