/* Copyright 2023, 2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { type IOpenIDToken, type MatrixClient } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/lib/logger"; import { FailToGetOpenIdToken } from "../utils/errors"; import { doNetworkOperationWithRetry } from "../utils/matrix"; /** * Configuration and access tokens provided by the SFU on successful authentication. */ export interface SFUConfig { url: string; jwt: string; livekitAlias: string; livekitIdentity: string; } /** * Decoded details from the JWT. */ interface SFUJWTPayload { /** * Expiration time for the JWT. * Note: This value is in seconds since Unix epoch. */ exp: number; /** * Name of the instance which authored the JWT */ iss: string; /** * Time at which the JWT can start to be used. * Note: This value is in seconds since Unix epoch. */ nbf: number; /** * Subject. The Livekit alias in this context. */ sub: string; /** * The set of permissions for the user. */ video: { canPublish: boolean; canSubscribe: boolean; room: string; roomJoin: boolean; }; } // The bits we need from MatrixClient export type OpenIDClientParts = Pick< MatrixClient, "getOpenIdToken" | "getDeviceId" >; /** * Gets a bearer token from the homeserver and then use it to authenticate * to the matrix RTC backend in order to get acces to the SFU. * It has built-in retry for calls to the homeserver with a backoff policy. * @param client * @param serviceUrl * @param matrixRoomId * @returns Object containing the token information * @throws FailToGetOpenIdToken */ export async function getSFUConfigWithOpenID( client: OpenIDClientParts, serviceUrl: string, matrixRoomId: string, ): Promise { let openIdToken: IOpenIDToken; try { openIdToken = await doNetworkOperationWithRetry(async () => client.getOpenIdToken(), ); } catch (error) { throw new FailToGetOpenIdToken( error instanceof Error ? error : new Error("Unknown error"), ); } logger.debug("Got openID token", openIdToken); logger.info(`Trying to get JWT for focus ${serviceUrl}...`); const sfuConfig = await getLiveKitJWT( client, serviceUrl, matrixRoomId, openIdToken, ); logger.info(`Got JWT from call's active focus URL.`); // Pull the details from the JWT const [, payloadStr] = sfuConfig.jwt.split("."); // TODO: Prefer Uint8Array.fromBase64 when widely available const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload; return { jwt: sfuConfig.jwt, url: sfuConfig.url, livekitAlias: payload.video.room, // NOTE: Currently unused. livekitIdentity: payload.sub, }; } async function getLiveKitJWT( client: OpenIDClientParts, livekitServiceURL: string, roomName: string, openIDToken: IOpenIDToken, ): Promise<{ url: string; jwt: string }> { try { const res = await fetch(livekitServiceURL + "/sfu/get", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ room: roomName, openid_token: openIDToken, device_id: client.getDeviceId(), }), }); if (!res.ok) { throw new Error("SFU Config fetch failed with status code " + res.status); } return await res.json(); } catch (e) { throw new Error("SFU Config fetch failed with exception", { cause: e }); } }