Merge pull request #5173 from matrix-org/jaywink/jitsi-openidjwt-auth

Support creation of Jitsi widgets with "openidtoken-jwt" auth
This commit is contained in:
Jason Robinson 2020-09-08 17:16:50 +03:00 committed by GitHub
commit 75518254fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 104 additions and 10 deletions

View file

@ -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",

View file

@ -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 = (

View file

@ -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 {

View file

@ -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) {

View file

@ -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<string|null> {
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);

View file

@ -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<any>;
private readonly readyPromise: Promise<any>;
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(<ToWidgetRequest>payload, {});
});
} else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) {
// Save OpenID credentials
this.setOpenIDCredentials(<ToWidgetRequest>payload);
this.replyToRequest(<ToWidgetRequest>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<any> {
return this.readyPromise;
}

View file

@ -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"