Refactor how we aquire the jwt token for the local user. (only fetch it

once)

The local jwt token needs to be aquired via the right endpoint. The
endpoint defines how our rtcBackendIdentity is computed. Based on us
using sticky events or state events we also need to use the right
endpoint. This cannot be done generically in the connection manager. The
jwt token now is computed in the localTransport and the resolved sfu
config is passed to the connection manager.

Add JWT endpoint version and SFU config support Pin matrix-js-sdk to a
specific commit and update dev auth image tag. Propagate SFU config and
JWT endpoint choice through local transport, ConnectionManager and
Connection; add JwtEndpointVersion enum and LocalTransportWithSFUConfig
type. Add NO_MATRIX_2 auth error and locale string, thread
rtcBackendIdentity through UI props, and include related test, CSS and
minor imports updates
This commit is contained in:
Timo K
2026-01-09 13:38:26 +01:00
parent d4b06b0f9c
commit 7dbbd763b9
27 changed files with 421 additions and 192 deletions

View File

@@ -39,6 +39,7 @@ import { constant } from "../../Behavior";
import { ConnectionManagerData } from "../remoteMembers/ConnectionManager";
import { ConnectionState, type Connection } from "../remoteMembers/Connection";
import { type Publisher } from "./Publisher";
import { type LocalTransportWithSFUConfig } from "./LocalTransport";
const MATRIX_RTC_MODE = MatrixRTCMode.Legacy;
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
@@ -212,10 +213,11 @@ describe("LocalMembership", () => {
it("throws error on missing RTC config error", () => {
withTestScheduler(({ scope, hot, expectObservable }) => {
const localTransport$ = scope.behavior<null | LivekitTransport>(
hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")),
null,
);
const localTransport$ =
scope.behavior<null | LocalTransportWithSFUConfig>(
hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")),
null,
);
// we do not need any connection data since we want to fail before reaching that.
const mockConnectionManager = {
@@ -243,11 +245,23 @@ describe("LocalMembership", () => {
});
const aTransport = {
livekit_service_url: "a",
} as LivekitTransport;
transport: {
livekit_service_url: "a",
} as LivekitTransport,
sfuConfig: {
url: "sfu-url",
jwt: "sfu-token",
},
} as LocalTransportWithSFUConfig;
const bTransport = {
livekit_service_url: "b",
} as LivekitTransport;
transport: {
livekit_service_url: "b",
} as LivekitTransport,
sfuConfig: {
url: "sfu-url",
jwt: "sfu-token",
},
} as LocalTransportWithSFUConfig;
const connectionTransportAConnected = {
livekitRoom: mockLivekitRoom({
@@ -391,7 +405,8 @@ describe("LocalMembership", () => {
const scope = new ObservableScope();
const connectionManagerData = new ConnectionManagerData();
const localTransport$ = new BehaviorSubject<null | LivekitTransport>(null);
const localTransport$ =
new BehaviorSubject<null | LocalTransportWithSFUConfig>(null);
const connectionManagerData$ = new BehaviorSubject(
new Epoch(connectionManagerData),
);
@@ -468,7 +483,7 @@ describe("LocalMembership", () => {
});
(
connectionManagerData2.getConnectionForTransport(aTransport)!
connectionManagerData2.getConnectionForTransport(aTransport.transport)!
.state$ as BehaviorSubject<ConnectionState>
).next(ConnectionState.LivekitConnected);
expect(localMembership.localMemberState$.value).toStrictEqual({

View File

@@ -61,6 +61,7 @@ import {
} from "../remoteMembers/Connection.ts";
import { type HomeserverConnected } from "./HomeserverConnected.ts";
import { and$ } from "../../../utils/observable.ts";
import { type LocalTransportWithSFUConfig } from "./LocalTransport.ts";
export enum TransportState {
/** Not even a transport is available to the LocalMembership */
@@ -126,7 +127,7 @@ interface Props {
createPublisherFactory: (connection: Connection) => Publisher;
joinMatrixRTC: (transport: LivekitTransport) => void;
homeserverConnected: HomeserverConnected;
localTransport$: Behavior<LivekitTransport | null>;
localTransport$: Behavior<LocalTransportWithSFUConfig | null>;
matrixRTCSession: Pick<
MatrixRTCSession,
"updateCallIntent" | "leaveRoomSession"
@@ -234,7 +235,9 @@ export const createLocalMembership$ = ({
return null;
}
return connectionData.getConnectionForTransport(localTransport);
return connectionData.getConnectionForTransport(
localTransport.transport,
);
}),
tap((connection) => {
logger.info(
@@ -533,7 +536,7 @@ export const createLocalMembership$ = ({
if (!shouldConnect) return;
try {
joinMatrixRTC(transport);
joinMatrixRTC(transport.transport);
} catch (error) {
logger.error("Error entering RTC session", error);
if (error instanceof Error)

View File

@@ -19,7 +19,7 @@ import { BehaviorSubject, lastValueFrom } from "rxjs";
import fetchMock from "fetch-mock";
import { mockConfig, flushPromises, ownMemberMock } from "../../../utils/test";
import { createLocalTransport$ } from "./LocalTransport";
import { createLocalTransport$, JwtEndpointVersion } from "./LocalTransport";
import { constant } from "../../Behavior";
import { Epoch, ObservableScope } from "../../ObservableScope";
import {
@@ -58,7 +58,7 @@ describe("LocalTransport", () => {
getDeviceId: vi.fn(),
},
ownMembershipIdentity: ownMemberMock,
useOldJwtEndpoint$: constant(false),
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant("delay_id_mock"),
});
await flushPromises();
@@ -98,7 +98,7 @@ describe("LocalTransport", () => {
getDeviceId: vi.fn(),
},
ownMembershipIdentity: ownMemberMock,
useOldJwtEndpoint$: constant(false),
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant("delay_id_mock"),
});
localTransport$.subscribe(
@@ -140,7 +140,7 @@ describe("LocalTransport", () => {
baseUrl: "https://lk.example.org",
},
ownMembershipIdentity: ownMemberMock,
useOldJwtEndpoint$: constant(false),
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant("delay_id_mock"),
});
@@ -186,7 +186,7 @@ describe("LocalTransport", () => {
baseUrl: "https://lk.example.org",
},
ownMembershipIdentity: ownMemberMock,
useOldJwtEndpoint$: constant(false),
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant("delay_id_mock"),
});
@@ -216,7 +216,7 @@ describe("LocalTransport", () => {
scope,
roomId: "!example_room_id",
useOldestMember$: constant(false),
useOldJwtEndpoint$: constant(false),
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant(null),
memberships$: constant(new Epoch<CallMembership[]>([])),
client: {
@@ -333,7 +333,7 @@ describe("LocalTransport", () => {
ownMembershipIdentity: ownMemberMock,
roomId: "!example_room_id",
useOldestMember$: constant(false),
useOldJwtEndpoint$: constant(false),
forceJwtEndpoint$: constant(JwtEndpointVersion.Legacy),
delayId$: constant(null),
memberships$: constant(new Epoch<CallMembership[]>([])),
client: {

View File

@@ -31,9 +31,11 @@ import { Config } from "../../../config/Config.ts";
import {
FailToGetOpenIdToken,
MatrixRTCTransportMissingError,
NoMatrix2AuthorizationService,
} from "../../../utils/errors.ts";
import {
getSFUConfigWithOpenID,
type SFUConfig,
type OpenIDClientParts,
} from "../../../livekit/openIDSFU.ts";
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
@@ -57,10 +59,25 @@ interface Props {
OpenIDClientParts;
roomId: string;
useOldestMember$: Behavior<boolean>;
useOldJwtEndpoint$: Behavior<boolean>;
forceJwtEndpoint$: Behavior<JwtEndpointVersion>;
delayId$: Behavior<string | null>;
}
export enum JwtEndpointVersion {
Legacy = "legacy",
Matrix_2_0 = "matrix_2_0",
}
export interface LocalTransportWithSFUConfig {
transport: LivekitTransport;
sfuConfig: SFUConfig;
}
export function isLocalTransportWithSFUConfig(
obj: LivekitTransport | LocalTransportWithSFUConfig,
): obj is LocalTransportWithSFUConfig {
return "transport" in obj && "sfuConfig" in obj;
}
/**
* This class is responsible for managing the local transport.
* "Which transport is the local member going to use"
@@ -81,22 +98,40 @@ export const createLocalTransport$ = ({
client,
roomId,
useOldestMember$,
useOldJwtEndpoint$,
forceJwtEndpoint$,
delayId$,
}: Props): Behavior<LivekitTransport | null> => {
}: Props): Behavior<LocalTransportWithSFUConfig | null> => {
/**
* The transport over which we should be actively publishing our media.
* undefined when not joined.
*/
const oldestMemberTransport$ = scope.behavior(
combineLatest([memberships$, useOldJwtEndpoint$]).pipe(
map(([memberships, forceOldJwtEndpoint]) => {
combineLatest([memberships$]).pipe(
map(([memberships]) => {
const oldestMember = memberships.value[0];
const transport = oldestMember?.getTransport(memberships.value[0]);
if (!transport) return null;
return transport;
}),
first((t) => t != null && isLivekitTransport(t)),
switchMap((transport) => {
// Get the open jwt token to connect to the sfu
const computeLocalTransportWithSFUConfig =
async (): Promise<LocalTransportWithSFUConfig> => {
return {
transport,
sfuConfig: await getSFUConfigWithOpenID(
client,
ownMembershipIdentity,
transport.livekit_service_url,
roomId,
{ forceJwtEndpoint: JwtEndpointVersion.Legacy },
logger,
),
};
};
return from(computeLocalTransportWithSFUConfig());
}),
),
null,
);
@@ -108,19 +143,29 @@ export const createLocalTransport$ = ({
* @throws MatrixRTCTransportMissingError | FailToGetOpenIdToken
*/
const preferredTransport$ = scope.behavior(
combineLatest([customLivekitUrl.value$, delayId$, useOldJwtEndpoint$]).pipe(
switchMap(([customUrl, delayId, forceOldJwtEndpoint]) =>
from(
// preferredTransport$ (used for multi sfu) needs to know if we are using the old or new
// jwt endpoint (`get_token` vs `sfu/get`) based on that the jwt endpoint will compute the rtcBackendIdentity
// differently. (sha(`${userId}|${deviceId}|${memberId}`) vs `${userId}|${deviceId}|${memberId}`)
// When using sticky events (we need to use the new endpoint).
combineLatest([customLivekitUrl.value$, delayId$, forceJwtEndpoint$]).pipe(
switchMap(([customUrl, delayId, forceEndpoint]) => {
logger.info(
"Creating preferred transport based on: ",
customUrl,
delayId,
forceEndpoint,
);
return from(
makeTransport(
client,
ownMembershipIdentity,
roomId,
customUrl,
forceOldJwtEndpoint,
forceEndpoint,
delayId ?? undefined,
),
),
),
);
}),
),
null,
);
@@ -139,7 +184,9 @@ export const createLocalTransport$ = ({
? (oldestMemberTransport ?? preferredTransport)
: preferredTransport,
),
distinctUntilChanged((t1, t2) => areLivekitTransportsEqual(t1, t2)),
distinctUntilChanged((t1, t2) =>
areLivekitTransportsEqual(t1?.transport ?? null, t2?.transport ?? null),
),
),
);
};
@@ -161,7 +208,10 @@ const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
* @param membership The membership identity of the user.
* @param roomId The ID of the room to be connected to.
* @param urlFromDevSettings Override URL provided by the user's local config.
* @param forceOldJwtEndpoint Whether to force the old JWT endpoint (not hashing the backendIdentity).
* @param forceJwtEndpoint Whether to force a specific JWT endpoint
* - `Legacy` / `Matrix_2_0`
* - `get_token` / `sfu/get`
* - not hashing / hashing the backendIdentity
* @param delayId the delay id passed to the jwt service.
*
* @returns A fully validated transport config.
@@ -176,26 +226,33 @@ async function makeTransport(
membership: CallMembershipIdentityParts,
roomId: string,
urlFromDevSettings: string | null,
forceOldJwtEndpoint: boolean,
forceJwtEndpoint: JwtEndpointVersion,
delayId?: string,
): Promise<LivekitTransport> {
): Promise<LocalTransportWithSFUConfig> {
logger.trace("Searching for a preferred transport");
async function doOpenIdAndJWTFromUrl(url: string): Promise<LivekitTransport> {
const { livekitAlias } = await getSFUConfigWithOpenID(
async function doOpenIdAndJWTFromUrl(
url: string,
): Promise<LocalTransportWithSFUConfig> {
const sfuConfig = await getSFUConfigWithOpenID(
client,
membership,
url,
forceOldJwtEndpoint,
roomId,
client.baseUrl,
delayId,
{
forceJwtEndpoint: forceJwtEndpoint,
delayEndpointBaseUrl: client.baseUrl,
delayId,
},
logger,
);
return {
type: "livekit",
livekit_service_url: url,
livekit_alias: livekitAlias,
transport: {
type: "livekit",
livekit_service_url: url,
livekit_alias: sfuConfig.livekitAlias,
},
sfuConfig,
};
}
// We will call `getSFUConfigWithOpenID` once per transport here as it's our
@@ -217,7 +274,7 @@ async function makeTransport(
async function getFirstUsableTransport(
transports: Transport[],
): Promise<LivekitTransport | null> {
): Promise<LocalTransportWithSFUConfig | null> {
for (const potentialTransport of transports) {
if (isLivekitTransportConfig(potentialTransport)) {
try {
@@ -226,8 +283,11 @@ async function makeTransport(
potentialTransport.livekit_service_url,
);
} catch (ex) {
// Explictly throw these
if (ex instanceof FailToGetOpenIdToken) {
// Explictly throw these
throw ex;
}
if (ex instanceof NoMatrix2AuthorizationService) {
throw ex;
}
logger.debug(