diff --git a/package.json b/package.json index d6d7140a09..0a151e0e28 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "browser-request": "^0.3.3", "gfm.css": "^1.1.2", "highlight.js": "^9.13.1", + "jsrsasign": "^9.1.5", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-react-sdk": "github:matrix-org/matrix-react-sdk#develop", "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz", diff --git a/src/vector/jitsi/index.html b/src/vector/jitsi/index.html index b9fc950e15..1259eb1c4a 100644 --- a/src/vector/jitsi/index.html +++ b/src/vector/jitsi/index.html @@ -11,7 +11,9 @@

Jitsi Video Conference

- +
+ +
diff --git a/src/vector/jitsi/index.ts b/src/vector/jitsi/index.ts index 081246c0a9..6e697fee2f 100644 --- a/src/vector/jitsi/index.ts +++ b/src/vector/jitsi/index.ts @@ -18,7 +18,10 @@ limitations under the License. require("./index.scss"); import * as qs from 'querystring'; -import { Capability, WidgetApi } from "matrix-react-sdk/src/widgets/WidgetApi"; +import {Capability, WidgetApi} from 'matrix-react-sdk/src/widgets/WidgetApi'; +import {KJUR} from 'jsrsasign'; + +const JITSI_OPENIDTOKEN_JWT_AUTH = 'openidtoken-jwt'; // Dev note: we use raw JS without many dependencies to reduce bundle size. // We do not need all of React to render a Jitsi conference. @@ -33,6 +36,8 @@ let conferenceId: string; let displayName: string; let avatarUrl: string; let userId: string; +let jitsiAuth: string; +let roomId: string; let widgetApi: WidgetApi; @@ -69,40 +74,118 @@ let widgetApi: WidgetApi; displayName = qsParam('displayName', true); avatarUrl = qsParam('avatarUrl', true); // http not mxc userId = qsParam('userId'); + jitsiAuth = qsParam('auth', true); + roomId = qsParam('roomId', true); if (widgetApi) { await widgetApi.waitReady(); await widgetApi.setAlwaysOnScreen(false); // start off as detachable from the screen + + // See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification + if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) { + // Request credentials, give callback to continue when received + widgetApi.requestOpenIDCredentials(credentialsResponseCallback); + } else { + enableJoinButton(); + } + // TODO: register widgetApi listeners for PTT controls (https://github.com/vector-im/riot-web/issues/12795) + } else { + enableJoinButton(); } - - // TODO: register widgetApi listeners for PTT controls (https://github.com/vector-im/riot-web/issues/12795) - - document.getElementById("joinButton").onclick = () => joinConference(); } catch (e) { console.error("Error setting up Jitsi widget", e); - document.getElementById("jitsiContainer").innerText = "Failed to load Jitsi widget"; - switchVisibleContainers(); + document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget"; } })(); +/** + * Enable or show error depending on what the credentials response is. + */ +function credentialsResponseCallback() { + if (widgetApi.openIDCredentials) { + console.info('Successfully got OpenID credentials.'); + enableJoinButton(); + } else { + console.warn('OpenID credentials request was blocked by user.'); + document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget"; + } +} + +function enableJoinButton() { + document.getElementById("joinButton").onclick = () => joinConference(); +} + function switchVisibleContainers() { inConference = !inConference; document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden'; document.getElementById("joinButtonContainer").style.visibility = inConference ? 'hidden' : 'unset'; } +/** + * Create a JWT token fot jitsi openidtoken-jwt auth + * + * See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification + */ +function createJWTToken() { + // Header + const header = {alg: 'HS256', typ: 'JWT'}; + // Payload + const payload = { + // As per Jitsi token auth, `iss` needs to be set to something agreed between + // JWT generating side and Prosody config. Since we have no configuration for + // the widgets, we can't set one anywhere. Using the Jitsi domain here probably makes sense. + iss: jitsiDomain, + sub: jitsiDomain, + aud: `https://${jitsiDomain}`, + room: "*", + context: { + matrix: { + token: widgetApi.openIDCredentials.accessToken, + room_id: roomId, + }, + user: { + avatar: avatarUrl, + name: displayName, + }, + }, + }; + // Sign JWT + // The secret string here is irrelevant, we're only using the JWT + // to transport data to Prosody in the Jitsi stack. + return KJUR.jws.JWS.sign( + 'HS256', + JSON.stringify(header), + JSON.stringify(payload), + 'notused', + ); +} + function joinConference() { // event handler bound in HTML + let jwt; + if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) { + if (!widgetApi.openIDCredentials || !widgetApi.openIDCredentials.accessToken) { + // We've failing to get a token, don't try to init conference + console.warn('Expected to have an OpenID credential, cannot initialize widget.'); + document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget"; + return; + } + jwt = createJWTToken(); + } + switchVisibleContainers(); - // noinspection JSIgnoredPromiseFromCall - if (widgetApi) widgetApi.setAlwaysOnScreen(true); // ignored promise because we don't care if it works + if (widgetApi) { + // ignored promise because we don't care if it works + // noinspection JSIgnoredPromiseFromCall + widgetApi.setAlwaysOnScreen(true); + } console.warn( "[Jitsi Widget] The next few errors about failing to parse URL parameters are fine if " + "they mention 'external_api' or 'jitsi' in the stack. They're just Jitsi Meet trying to parse " + "our fragment values and not recognizing the options.", ); - const meetApi = new JitsiMeetExternalAPI(jitsiDomain, { + const options = { width: "100%", height: "100%", parentNode: document.querySelector("#jitsiContainer"), @@ -113,7 +196,10 @@ function joinConference() { // event handler bound in HTML MAIN_TOOLBAR_BUTTONS: [], VIDEO_LAYOUT_FIT: "height", }, - }); + jwt: jwt, + }; + + const meetApi = new JitsiMeetExternalAPI(jitsiDomain, options); if (displayName) meetApi.executeCommand("displayName", displayName); if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl); if (userId) meetApi.executeCommand("email", userId); @@ -121,8 +207,11 @@ function joinConference() { // event handler bound in HTML meetApi.on("readyToClose", () => { switchVisibleContainers(); - // noinspection JSIgnoredPromiseFromCall - if (widgetApi) widgetApi.setAlwaysOnScreen(false); // ignored promise because we don't care if it works + if (widgetApi) { + // ignored promise because we don't care if it works + // noinspection JSIgnoredPromiseFromCall + widgetApi.setAlwaysOnScreen(false); + } document.getElementById("jitsiContainer").innerHTML = ""; }); diff --git a/yarn.lock b/yarn.lock index 9b1342e5e6..441250b72c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6914,6 +6914,11 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jsrsasign@^9.1.5: + version "9.1.5" + resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-9.1.5.tgz#fe286425d2c05b2d0865d24ded53e34b12abd2ca" + integrity sha512-iJLF8FvZHlwyQudrRtQomHj1HdPAcM8QSRTt0FJo8a6iFgaGCpKUrE7lWyELpAjrFs8jUC/Azc0vfhlj3yqHPQ== + jsx-ast-utils@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.3.0.tgz#edd727794ea284d7fda575015ed1b0cde0289ab6"