diff --git a/package.json b/package.json index 9b7d80ca73..b85191dc22 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,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/src/CallHandler.js b/src/CallHandler.js index 18f6aeb98a..27e8e34e16 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,10 +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. + // More details here: + // 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 + 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); @@ -403,6 +415,7 @@ async function _startCallApp(roomId, type) { conferenceId: confId, isAudioOnly: type === 'voice', domain: jitsiDomain, + auth: jitsiAuth, }; const widgetId = ( diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 01366091fd..22a2808eb6 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -626,7 +626,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 +640,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..d5f6981476 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 queryStringParts = [ '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) { + queryStringParts.push(`auth=${opts.auth}`); + } + const queryString = queryStringParts.join('&'); let baseUrl = window.location; if (window.location.protocol !== "https:" && !opts.forLocalRender) { diff --git a/src/widgets/Jitsi.ts b/src/widgets/Jitsi.ts index a52f8182aa..ca8de4468a 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 https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification + */ + 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); diff --git a/src/widgets/WidgetApi.ts b/src/widgets/WidgetApi.ts index 15603e9437..672cbf2a56 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", @@ -64,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. * @@ -73,10 +81,12 @@ export interface FromWidgetRequest extends WidgetRequest { * 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; /** * Set this to true if your widget is expecting a ready message from the client. False otherwise (default). @@ -120,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}`); } @@ -134,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; } 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"