From 5d8a082eb198ac516b71cff974f390da977c8a52 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 4 Sep 2020 10:03:30 +0300 Subject: [PATCH 01/11] Add Jitsi auth check Checks for auth needed by looking up a well-known file from the preferred Jitsi domain. No file existing will assume no auth. --- src/widgets/Jitsi.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/widgets/Jitsi.ts b/src/widgets/Jitsi.ts index a52f8182aa..1805913ad6 100644 --- a/src/widgets/Jitsi.ts +++ b/src/widgets/Jitsi.ts @@ -34,6 +34,30 @@ export class Jitsi { return this.domain || 'jitsi.riot.im'; } + /** + * Checks for auth needed by looking up a well-known file + * + * If the file does not exist, we assume no auth. + * + * See TODO add link + */ + public async getJitsiAuth(): Promise { + if (!this.preferredDomain) { + return null; + } + let data; + try { + const response = await fetch(`https://${this.preferredDomain}/.well-known/element/jitsi`); + data = await response.json(); + } catch (error) { + return null; + } + if (data.auth) { + return data.auth; + } + return null; + } + public start() { const cli = MatrixClientPeg.get(); cli.on("WellKnown.client", this.update); From db2e1a9cd0b34a5ca700d329256794fd51c9c80d Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 4 Sep 2020 10:04:18 +0300 Subject: [PATCH 02/11] Add rfc4648 (base64/32/16) encoder to dependencies --- package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index fc5ed57a77..dd293fabf4 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "react-focus-lock": "^2.4.1", "react-transition-group": "^4.4.1", "resize-observer-polyfill": "^1.5.1", + "rfc4648": "^1.4.0", "sanitize-html": "^1.27.1", "tar-js": "^0.3.0", "text-encoding-utf-8": "^1.0.2", diff --git a/yarn.lock b/yarn.lock index 5bd2be1567..ec099bbf7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7557,6 +7557,11 @@ retry@^0.10.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= +rfc4648@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd" + integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg== + rimraf@2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" From 680de2af95c4319f97e69e2c6b8d55e6df6fd342 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 4 Sep 2020 10:05:47 +0300 Subject: [PATCH 03/11] Create Jitsi "openidtoken-jwt" auth compatible conference ID's If the Jitsi server we're using for a Jitsi conference call has auth of "openidtoken-jwt" then instead of a random human readable room ID, encode the room ID in base32 (without padding). This can then be decoded back to the room ID on the Jitsi end of things. --- src/CallHandler.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 18f6aeb98a..9d1760bf43 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017, 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -67,6 +67,7 @@ import {generateHumanReadableId} from "./utils/NamingUtils"; import {Jitsi} from "./widgets/Jitsi"; import {WidgetType} from "./widgets/WidgetType"; import {SettingLevel} from "./settings/SettingLevel"; +import {base32} from "rfc4648"; global.mxCalls = { //room_id: MatrixCall @@ -388,8 +389,21 @@ async function _startCallApp(roomId, type) { return; } - const confId = `JitsiConference${generateHumanReadableId()}`; const jitsiDomain = Jitsi.getInstance().preferredDomain; + const jitsiAuth = await Jitsi.getInstance().getJitsiAuth(); + let confId; + if (jitsiAuth === 'openidtoken-jwt') { + // Create conference ID from room ID + // For compatibility with Jitsi, use base32 without padding. + // If the room ID needs to be decoded from the conference ID, + // the receiver should first uppercase it if needed and then add padding. + // More details here: + // TODO add link + confId = base32.stringify(Buffer.from(roomId), { pad: false }); + } else { + // Create a random human readable conference ID + confId = `JitsiConference${generateHumanReadableId()}`; + } let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); From a511ad6633cd0ba9144146845bb9a607bb48624d Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 4 Sep 2020 10:20:58 +0300 Subject: [PATCH 04/11] Add (potential) Jitsi auth type to widget data So we don't have to fetch the auth type when joining the conference. --- src/CallHandler.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CallHandler.js b/src/CallHandler.js index 9d1760bf43..702613bb81 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -411,12 +411,14 @@ async function _startCallApp(roomId, type) { const parsedUrl = new URL(widgetUrl); parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead parsedUrl.searchParams.set('confId', confId); + parsedUrl.searchParams.set('auth', jitsiAuth); widgetUrl = parsedUrl.toString(); const widgetData = { conferenceId: confId, isAudioOnly: type === 'voice', domain: jitsiDomain, + auth: jitsiAuth, }; const widgetId = ( From baa6d8a29430348ace9a5e67d19468d6c6a2d51b Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 4 Sep 2020 12:42:46 +0300 Subject: [PATCH 05/11] Correctly template in Jitsi widget auth if such exists Also add roomId to the widget data and URL template. It's needed by the Element Web Jitsi code to produce auth for the Jitsi backend. --- src/CallHandler.js | 4 ++-- src/components/views/elements/AppTile.js | 11 +++++++++-- src/utils/WidgetUtils.js | 11 ++++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 702613bb81..3c7e7fcc78 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -405,13 +405,12 @@ async function _startCallApp(roomId, type) { confId = `JitsiConference${generateHumanReadableId()}`; } - let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); + let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth}); // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets const parsedUrl = new URL(widgetUrl); parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead parsedUrl.searchParams.set('confId', confId); - parsedUrl.searchParams.set('auth', jitsiAuth); widgetUrl = parsedUrl.toString(); const widgetData = { @@ -419,6 +418,7 @@ async function _startCallApp(roomId, type) { isAudioOnly: type === 'voice', domain: jitsiDomain, auth: jitsiAuth, + roomId: roomId, }; const widgetId = ( diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 299025f949..9e0dd3c6c2 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -603,6 +603,7 @@ export default class AppTile extends React.Component { // TODO: Namespace themes through some standard 'theme': SettingsStore.getValue("theme"), }); + console.log("DEBUG TEMPLATEDURL APPTILE", vars); if (vars.conferenceId === undefined) { // we'll need to parse the conference ID out of the URL for v1 Jitsi widgets @@ -626,7 +627,10 @@ export default class AppTile extends React.Component { if (WidgetType.JITSI.matches(this.props.app.type)) { console.log("Replacing Jitsi widget URL with local wrapper"); - url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true}); + url = WidgetUtils.getLocalJitsiWrapperUrl({ + forLocalRender: true, + auth: this.props.app.data ? this.props.app.data.auth : null, + }); url = this._addWurlParams(url); } else { url = this._getSafeUrl(this.state.widgetUrl); @@ -637,7 +641,10 @@ export default class AppTile extends React.Component { _getPopoutUrl() { if (WidgetType.JITSI.matches(this.props.app.type)) { return this._templatedUrl( - WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}), + WidgetUtils.getLocalJitsiWrapperUrl({ + forLocalRender: false, + auth: this.props.app.data ? this.props.app.data.auth : null, + }), this.props.app.type, ); } else { diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 645953210d..c9666d90d5 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -448,16 +448,21 @@ export default class WidgetUtils { return encodeURIComponent(`${widgetLocation}::${widgetUrl}`); } - static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean}={}) { + static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string}={}) { // NB. we can't just encodeURIComponent all of these because the $ signs need to be there - const queryString = [ + const queryParts = [ 'conferenceDomain=$domain', 'conferenceId=$conferenceId', 'isAudioOnly=$isAudioOnly', 'displayName=$matrix_display_name', 'avatarUrl=$matrix_avatar_url', 'userId=$matrix_user_id', - ].join('&'); + 'roomId=$matrix_room_id', + ]; + if (opts.auth) { + queryParts.push(`auth=${opts.auth}`); + } + const queryString = queryParts.join('&'); let baseUrl = window.location; if (window.location.protocol !== "https:" && !opts.forLocalRender) { From 13dbfa6b8560ade438d488f98900e99f9438f847 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Fri, 4 Sep 2020 12:44:43 +0300 Subject: [PATCH 06/11] Remove debug statement --- src/components/views/elements/AppTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 9e0dd3c6c2..dd1082e6c3 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -603,7 +603,6 @@ export default class AppTile extends React.Component { // TODO: Namespace themes through some standard 'theme': SettingsStore.getValue("theme"), }); - console.log("DEBUG TEMPLATEDURL APPTILE", vars); if (vars.conferenceId === undefined) { // we'll need to parse the conference ID out of the URL for v1 Jitsi widgets From ae83222d52749f473481fbb6e6807aad2e86d501 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Mon, 7 Sep 2020 14:32:00 +0300 Subject: [PATCH 07/11] Add GetOpenIDCredentials constant to WidgetApi --- src/widgets/WidgetApi.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 15603e9437..74351720e2 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -34,6 +34,7 @@ export enum KnownWidgetActions { GetCapabilities = "capabilities", SendEvent = "send_event", UpdateVisibility = "visibility", + GetOpenIDCredentials = "get_openid", ReceiveOpenIDCredentials = "openid_credentials", SetAlwaysOnScreen = "set_always_on_screen", ClientReady = "im.vector.ready", From d2e9ea58fde083564cfa5529ec988f6f89768b8e Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Mon, 7 Sep 2020 19:22:40 +0300 Subject: [PATCH 08/11] Add links to prosody openidtoken-jwt auth docs --- src/CallHandler.js | 4 +--- src/widgets/Jitsi.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 3c7e7fcc78..1a1c71b55f 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -395,10 +395,8 @@ async function _startCallApp(roomId, type) { if (jitsiAuth === 'openidtoken-jwt') { // Create conference ID from room ID // For compatibility with Jitsi, use base32 without padding. - // If the room ID needs to be decoded from the conference ID, - // the receiver should first uppercase it if needed and then add padding. // More details here: - // TODO add link + // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification confId = base32.stringify(Buffer.from(roomId), { pad: false }); } else { // Create a random human readable conference ID diff --git a/src/widgets/Jitsi.ts b/src/widgets/Jitsi.ts index 1805913ad6..ca8de4468a 100644 --- a/src/widgets/Jitsi.ts +++ b/src/widgets/Jitsi.ts @@ -39,7 +39,7 @@ export class Jitsi { * * If the file does not exist, we assume no auth. * - * See TODO add link + * See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification */ public async getJitsiAuth(): Promise { if (!this.preferredDomain) { From 4b43e39d2aa0bf151384ed910b4a7df6c92bc4cb Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 8 Sep 2020 11:31:40 +0300 Subject: [PATCH 09/11] Code review related changes * drop room ID from jitsi widget data * reame queryParts variable --- src/CallHandler.js | 1 - src/utils/WidgetUtils.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 1a1c71b55f..27e8e34e16 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -416,7 +416,6 @@ async function _startCallApp(roomId, type) { isAudioOnly: type === 'voice', domain: jitsiDomain, auth: jitsiAuth, - roomId: roomId, }; const widgetId = ( diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index c9666d90d5..d5f6981476 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -450,7 +450,7 @@ export default class WidgetUtils { static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string}={}) { // NB. we can't just encodeURIComponent all of these because the $ signs need to be there - const queryParts = [ + const queryStringParts = [ 'conferenceDomain=$domain', 'conferenceId=$conferenceId', 'isAudioOnly=$isAudioOnly', @@ -460,9 +460,9 @@ export default class WidgetUtils { 'roomId=$matrix_room_id', ]; if (opts.auth) { - queryParts.push(`auth=${opts.auth}`); + queryStringParts.push(`auth=${opts.auth}`); } - const queryString = queryParts.join('&'); + const queryString = queryStringParts.join('&'); let baseUrl = window.location; if (window.location.protocol !== "https:" && !opts.forLocalRender) { From c19336591e4cb82355bd93be7c3c062c3de9b71a Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 8 Sep 2020 12:59:05 +0300 Subject: [PATCH 10/11] Add OpenID token request flow to WidgetApi As per MSC1960. --- src/widgets/WidgetApi.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 74351720e2..4099793500 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -65,6 +65,13 @@ export interface FromWidgetRequest extends WidgetRequest { response: any; } +export interface OpenIDCredentials { + accessToken: string; + tokenType: string; + matrixServerName: string; + expiresIn: number; +} + /** * Handles Element <--> Widget interactions for embedded/standalone widgets. * @@ -78,6 +85,8 @@ export class WidgetApi extends EventEmitter { private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {}; private readyPromise: Promise; private readyPromiseResolve: () => void; + private openIDCredentialsCallback: () => void; + public openIDCredentials: OpenIDCredentials; /** * Set this to true if your widget is expecting a ready message from the client. False otherwise (default). @@ -121,6 +130,10 @@ export class WidgetApi extends EventEmitter { // Acknowledge that we're shut down now this.replyToRequest(payload, {}); }); + } else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) { + // Save OpenID credentials + this.setOpenIDCredentials(payload); + this.replyToRequest(payload, {}); } else { console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`); } @@ -135,6 +148,32 @@ export class WidgetApi extends EventEmitter { }); } + public setOpenIDCredentials(value: WidgetRequest) { + const data = value.data; + if (data.state === 'allowed') { + this.openIDCredentials = { + accessToken: data.access_token, + tokenType: data.token_type, + matrixServerName: data.matrix_server_name, + expiresIn: data.expires_in, + } + } else if (data.state === 'blocked') { + this.openIDCredentials = null; + } + if (['allowed', 'blocked'].includes(data.state) && this.openIDCredentialsCallback) { + this.openIDCredentialsCallback() + } + } + + public requestOpenIDCredentials(credentialsResponseCallback: () => void) { + this.openIDCredentialsCallback = credentialsResponseCallback; + this.callAction( + KnownWidgetActions.GetOpenIDCredentials, + {}, + this.setOpenIDCredentials, + ); + } + public waitReady(): Promise { return this.readyPromise; } From 3af0d33e3b775f22217982667913a166f19175e1 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 8 Sep 2020 13:00:00 +0300 Subject: [PATCH 11/11] Make a few fields readonly As they're only set in the constructor. --- src/widgets/WidgetApi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 4099793500..672cbf2a56 100644 --- a/src/widgets/WidgetApi.ts +++ b/src/widgets/WidgetApi.ts @@ -81,9 +81,9 @@ export interface OpenIDCredentials { * the given promise resolves. */ export class WidgetApi extends EventEmitter { - private origin: string; + private readonly origin: string; private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {}; - private readyPromise: Promise; + private readonly readyPromise: Promise; private readyPromiseResolve: () => void; private openIDCredentialsCallback: () => void; public openIDCredentials: OpenIDCredentials;