Merge pull request #15114 from vector-im/jaywink/jitsi-openidjwt-auth

Support usage of Jitsi widgets with "openidtoken-jwt" auth
This commit is contained in:
Travis Ralston 2020-09-08 08:19:07 -06:00 committed by GitHub
commit da0afeda9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 14 deletions

View File

@ -59,6 +59,7 @@
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"gfm.css": "^1.1.2", "gfm.css": "^1.1.2",
"highlight.js": "^9.13.1", "highlight.js": "^9.13.1",
"jsrsasign": "^9.1.5",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#develop", "matrix-react-sdk": "github:matrix-org/matrix-react-sdk#develop",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz", "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",

View File

@ -11,10 +11,12 @@
<div class="joinConferencePrompt"> <div class="joinConferencePrompt">
<!-- TODO: i18n --> <!-- TODO: i18n -->
<h2>Jitsi Video Conference</h2> <h2>Jitsi Video Conference</h2>
<div id="widgetActionContainer">
<button type="button" id="joinButton">Join Conference</button> <button type="button" id="joinButton">Join Conference</button>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- This script is not webpacked, and the script is downloaded at build time --> <!-- This script is not webpacked, and the script is downloaded at build time -->
<script src="./jitsi_external_api.min.js"></script> <script src="./jitsi_external_api.min.js"></script>
</body> </body>

View File

@ -18,7 +18,10 @@ limitations under the License.
require("./index.scss"); require("./index.scss");
import * as qs from 'querystring'; 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. // 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. // We do not need all of React to render a Jitsi conference.
@ -33,6 +36,8 @@ let conferenceId: string;
let displayName: string; let displayName: string;
let avatarUrl: string; let avatarUrl: string;
let userId: string; let userId: string;
let jitsiAuth: string;
let roomId: string;
let widgetApi: WidgetApi; let widgetApi: WidgetApi;
@ -69,40 +74,118 @@ let widgetApi: WidgetApi;
displayName = qsParam('displayName', true); displayName = qsParam('displayName', true);
avatarUrl = qsParam('avatarUrl', true); // http not mxc avatarUrl = qsParam('avatarUrl', true); // http not mxc
userId = qsParam('userId'); userId = qsParam('userId');
jitsiAuth = qsParam('auth', true);
roomId = qsParam('roomId', true);
if (widgetApi) { if (widgetApi) {
await widgetApi.waitReady(); await widgetApi.waitReady();
await widgetApi.setAlwaysOnScreen(false); // start off as detachable from the screen 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) // TODO: register widgetApi listeners for PTT controls (https://github.com/vector-im/riot-web/issues/12795)
} else {
document.getElementById("joinButton").onclick = () => joinConference(); enableJoinButton();
}
} catch (e) { } catch (e) {
console.error("Error setting up Jitsi widget", e); console.error("Error setting up Jitsi widget", e);
document.getElementById("jitsiContainer").innerText = "Failed to load Jitsi widget"; document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget";
switchVisibleContainers();
} }
})(); })();
/**
* 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() { function switchVisibleContainers() {
inConference = !inConference; inConference = !inConference;
document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden'; document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden';
document.getElementById("joinButtonContainer").style.visibility = inConference ? 'hidden' : 'unset'; 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 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(); switchVisibleContainers();
if (widgetApi) {
// ignored promise because we don't care if it works
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall
if (widgetApi) widgetApi.setAlwaysOnScreen(true); // ignored promise because we don't care if it works widgetApi.setAlwaysOnScreen(true);
}
console.warn( console.warn(
"[Jitsi Widget] The next few errors about failing to parse URL parameters are fine if " + "[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 " + "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.", "our fragment values and not recognizing the options.",
); );
const meetApi = new JitsiMeetExternalAPI(jitsiDomain, { const options = {
width: "100%", width: "100%",
height: "100%", height: "100%",
parentNode: document.querySelector("#jitsiContainer"), parentNode: document.querySelector("#jitsiContainer"),
@ -113,7 +196,10 @@ function joinConference() { // event handler bound in HTML
MAIN_TOOLBAR_BUTTONS: [], MAIN_TOOLBAR_BUTTONS: [],
VIDEO_LAYOUT_FIT: "height", VIDEO_LAYOUT_FIT: "height",
}, },
}); jwt: jwt,
};
const meetApi = new JitsiMeetExternalAPI(jitsiDomain, options);
if (displayName) meetApi.executeCommand("displayName", displayName); if (displayName) meetApi.executeCommand("displayName", displayName);
if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl); if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl);
if (userId) meetApi.executeCommand("email", userId); if (userId) meetApi.executeCommand("email", userId);
@ -121,8 +207,11 @@ function joinConference() { // event handler bound in HTML
meetApi.on("readyToClose", () => { meetApi.on("readyToClose", () => {
switchVisibleContainers(); switchVisibleContainers();
if (widgetApi) {
// ignored promise because we don't care if it works
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall
if (widgetApi) widgetApi.setAlwaysOnScreen(false); // ignored promise because we don't care if it works widgetApi.setAlwaysOnScreen(false);
}
document.getElementById("jitsiContainer").innerHTML = ""; document.getElementById("jitsiContainer").innerHTML = "";
}); });

View File

@ -6914,6 +6914,11 @@ jsprim@^1.2.2:
json-schema "0.2.3" json-schema "0.2.3"
verror "1.10.0" 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: jsx-ast-utils@^2.2.3:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.3.0.tgz#edd727794ea284d7fda575015ed1b0cde0289ab6" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.3.0.tgz#edd727794ea284d7fda575015ed1b0cde0289ab6"