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:
commit
75518254fb
7 changed files with 104 additions and 10 deletions
|
@ -94,6 +94,7 @@
|
||||||
"react-focus-lock": "^2.4.1",
|
"react-focus-lock": "^2.4.1",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
|
"rfc4648": "^1.4.0",
|
||||||
"sanitize-html": "^1.27.1",
|
"sanitize-html": "^1.27.1",
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
"text-encoding-utf-8": "^1.0.2",
|
"text-encoding-utf-8": "^1.0.2",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017, 2018 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 {Jitsi} from "./widgets/Jitsi";
|
||||||
import {WidgetType} from "./widgets/WidgetType";
|
import {WidgetType} from "./widgets/WidgetType";
|
||||||
import {SettingLevel} from "./settings/SettingLevel";
|
import {SettingLevel} from "./settings/SettingLevel";
|
||||||
|
import {base32} from "rfc4648";
|
||||||
|
|
||||||
global.mxCalls = {
|
global.mxCalls = {
|
||||||
//room_id: MatrixCall
|
//room_id: MatrixCall
|
||||||
|
@ -388,10 +389,21 @@ async function _startCallApp(roomId, type) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const confId = `JitsiConference${generateHumanReadableId()}`;
|
|
||||||
const jitsiDomain = Jitsi.getInstance().preferredDomain;
|
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
|
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
|
||||||
const parsedUrl = new URL(widgetUrl);
|
const parsedUrl = new URL(widgetUrl);
|
||||||
|
@ -403,6 +415,7 @@ async function _startCallApp(roomId, type) {
|
||||||
conferenceId: confId,
|
conferenceId: confId,
|
||||||
isAudioOnly: type === 'voice',
|
isAudioOnly: type === 'voice',
|
||||||
domain: jitsiDomain,
|
domain: jitsiDomain,
|
||||||
|
auth: jitsiAuth,
|
||||||
};
|
};
|
||||||
|
|
||||||
const widgetId = (
|
const widgetId = (
|
||||||
|
|
|
@ -626,7 +626,10 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
console.log("Replacing Jitsi widget URL with local wrapper");
|
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);
|
url = this._addWurlParams(url);
|
||||||
} else {
|
} else {
|
||||||
url = this._getSafeUrl(this.state.widgetUrl);
|
url = this._getSafeUrl(this.state.widgetUrl);
|
||||||
|
@ -637,7 +640,10 @@ export default class AppTile extends React.Component {
|
||||||
_getPopoutUrl() {
|
_getPopoutUrl() {
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
return this._templatedUrl(
|
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,
|
this.props.app.type,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -448,16 +448,21 @@ export default class WidgetUtils {
|
||||||
return encodeURIComponent(`${widgetLocation}::${widgetUrl}`);
|
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
|
// NB. we can't just encodeURIComponent all of these because the $ signs need to be there
|
||||||
const queryString = [
|
const queryStringParts = [
|
||||||
'conferenceDomain=$domain',
|
'conferenceDomain=$domain',
|
||||||
'conferenceId=$conferenceId',
|
'conferenceId=$conferenceId',
|
||||||
'isAudioOnly=$isAudioOnly',
|
'isAudioOnly=$isAudioOnly',
|
||||||
'displayName=$matrix_display_name',
|
'displayName=$matrix_display_name',
|
||||||
'avatarUrl=$matrix_avatar_url',
|
'avatarUrl=$matrix_avatar_url',
|
||||||
'userId=$matrix_user_id',
|
'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;
|
let baseUrl = window.location;
|
||||||
if (window.location.protocol !== "https:" && !opts.forLocalRender) {
|
if (window.location.protocol !== "https:" && !opts.forLocalRender) {
|
||||||
|
|
|
@ -34,6 +34,30 @@ export class Jitsi {
|
||||||
return this.domain || 'jitsi.riot.im';
|
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() {
|
public start() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("WellKnown.client", this.update);
|
cli.on("WellKnown.client", this.update);
|
||||||
|
|
|
@ -34,6 +34,7 @@ export enum KnownWidgetActions {
|
||||||
GetCapabilities = "capabilities",
|
GetCapabilities = "capabilities",
|
||||||
SendEvent = "send_event",
|
SendEvent = "send_event",
|
||||||
UpdateVisibility = "visibility",
|
UpdateVisibility = "visibility",
|
||||||
|
GetOpenIDCredentials = "get_openid",
|
||||||
ReceiveOpenIDCredentials = "openid_credentials",
|
ReceiveOpenIDCredentials = "openid_credentials",
|
||||||
SetAlwaysOnScreen = "set_always_on_screen",
|
SetAlwaysOnScreen = "set_always_on_screen",
|
||||||
ClientReady = "im.vector.ready",
|
ClientReady = "im.vector.ready",
|
||||||
|
@ -64,6 +65,13 @@ export interface FromWidgetRequest extends WidgetRequest {
|
||||||
response: any;
|
response: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenIDCredentials {
|
||||||
|
accessToken: string;
|
||||||
|
tokenType: string;
|
||||||
|
matrixServerName: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles Element <--> Widget interactions for embedded/standalone widgets.
|
* Handles Element <--> Widget interactions for embedded/standalone widgets.
|
||||||
*
|
*
|
||||||
|
@ -73,10 +81,12 @@ export interface FromWidgetRequest extends WidgetRequest {
|
||||||
* the given promise resolves.
|
* the given promise resolves.
|
||||||
*/
|
*/
|
||||||
export class WidgetApi extends EventEmitter {
|
export class WidgetApi extends EventEmitter {
|
||||||
private origin: string;
|
private readonly origin: string;
|
||||||
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
|
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
|
||||||
private readyPromise: Promise<any>;
|
private readonly readyPromise: Promise<any>;
|
||||||
private readyPromiseResolve: () => void;
|
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).
|
* 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
|
// Acknowledge that we're shut down now
|
||||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
this.replyToRequest(<ToWidgetRequest>payload, {});
|
||||||
});
|
});
|
||||||
|
} else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) {
|
||||||
|
// Save OpenID credentials
|
||||||
|
this.setOpenIDCredentials(<ToWidgetRequest>payload);
|
||||||
|
this.replyToRequest(<ToWidgetRequest>payload, {});
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
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> {
|
public waitReady(): Promise<any> {
|
||||||
return this.readyPromise;
|
return this.readyPromise;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7557,6 +7557,11 @@ retry@^0.10.0:
|
||||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
|
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
|
||||||
integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
|
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:
|
rimraf@2.6.3:
|
||||||
version "2.6.3"
|
version "2.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue