mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
The OpenID flow moves to src/livekit/auth/openID.ts with the SFU config types in a sibling file, takes an options object, and replaces the MatrixRTC mode with the role we take on the transport plus the session's slot ID instead of a hard-coded m.call#ROOM. This changes behaviour: publishers on a livekit_service_url transport always use the legacy /sfu/get endpoint, so that subscribers can predict their user:device participant identity, and subscribers always use the default /get_token endpoint. The MatrixRTC mode no longer influences which endpoint is used. On the SDK this lands as sdk/src/media-backend/livekit/auth/ with the connection map, the discriminated connection state and the client-server API flow of the following commits of element-hq/element-call#4331, which rewrite the same files: - Model ConnectionState as a discriminated union - Show the whole transport in debug UI and logs (SDK part) - Let the publishing Connection authenticate itself - Reduce ConnectionManager to a ConnectionMap - Use the unstable LiveKit transport type - Identify transports by server name as well as URLs - Get SFU tokens and delegate delayed leaves through the MSC4195 CS API - Fall back to get_token as subscriber on widget API errors - Share a connection between homeservers using the same JWT service - Make the auth choice mode independent again Co-authored-by: Johannes Marbach <n0-0ne+github@mailbox.org> (cherry picked from commit adb380cd9ed108856e42d992fc03ced8c44f1e56)
230 lines
6.8 KiB
TypeScript
230 lines
6.8 KiB
TypeScript
/*
|
|
Copyright 2025 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import {
|
|
beforeEach,
|
|
afterEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
type MockedObject,
|
|
vitest,
|
|
} from "vitest";
|
|
import fetchMock from "fetch-mock";
|
|
import { MatrixError } from "matrix-js-sdk";
|
|
import { logger } from "matrix-js-sdk/lib/logger";
|
|
|
|
import {
|
|
getSFUConfig,
|
|
type GetSFUConfigParams,
|
|
type ClientOpenIDParts,
|
|
} from "./openID";
|
|
import { testJWTToken } from "../../../utils/test-fixtures";
|
|
import { ownMemberMock } from "../../../utils/test";
|
|
import { FailToGetOpenIdToken } from "../../../errors";
|
|
|
|
const serviceUrl = "https://sfu.example.org";
|
|
|
|
const delayParams = {
|
|
delayEndpointBaseUrl: "https://matrix.homeserverserver.org",
|
|
delayId: "mock_delay_id",
|
|
delayTimeoutMs: 3_600_000,
|
|
};
|
|
|
|
describe("getSFUConfig", () => {
|
|
let matrixClient: MockedObject<ClientOpenIDParts>;
|
|
let params: GetSFUConfigParams;
|
|
beforeEach(() => {
|
|
fetchMock.catch(404);
|
|
matrixClient = { getOpenIdToken: vitest.fn() };
|
|
params = {
|
|
client: matrixClient,
|
|
membership: ownMemberMock,
|
|
serviceUrl: "https://sfu.example.org",
|
|
roomId: "!example_room_id",
|
|
slotId: "m.call#ROOM",
|
|
role: "publisher",
|
|
logger,
|
|
};
|
|
});
|
|
afterEach(async () => {
|
|
void (await fetchMock.flush());
|
|
vitest.clearAllMocks();
|
|
fetchMock.reset();
|
|
});
|
|
|
|
it("should handle fetching a token", async () => {
|
|
fetchMock.post("https://sfu.example.org/sfu/get", () => {
|
|
return {
|
|
status: 200,
|
|
body: { url: serviceUrl, jwt: testJWTToken },
|
|
};
|
|
});
|
|
const config = await getSFUConfig(params);
|
|
expect(config).toEqual({
|
|
jwt: testJWTToken,
|
|
url: serviceUrl,
|
|
livekitIdentity: "@me:example.org:ABCDEF",
|
|
livekitAlias: "!example_room_id",
|
|
});
|
|
});
|
|
|
|
it("should fail if the SFU errors", async () => {
|
|
fetchMock.post("https://sfu.example.org/sfu/get", () => {
|
|
return {
|
|
status: 500,
|
|
body: {
|
|
errcode: "M_LOOKUP_FAILED",
|
|
error: "Failed to look up user info from homeserver",
|
|
},
|
|
};
|
|
});
|
|
try {
|
|
await getSFUConfig(params);
|
|
} catch (ex: unknown) {
|
|
expect(ex).toBeInstanceOf(FailToGetOpenIdToken);
|
|
expect((ex as FailToGetOpenIdToken).cause).toBeInstanceOf(MatrixError);
|
|
const mxError = (ex as Error).cause as MatrixError;
|
|
expect(mxError.message).toEqual(
|
|
"MatrixError: [500] Failed to look up user info from homeserver",
|
|
);
|
|
|
|
return;
|
|
}
|
|
expect.fail("Expected test to throw;");
|
|
});
|
|
|
|
it("should retry without delay params if the JWT service legacy endpoint returns M_BAD_JSON 400", async () => {
|
|
let callCount = 0;
|
|
|
|
fetchMock.post(
|
|
"https://sfu.example.org/sfu/get",
|
|
(url, opts) => {
|
|
callCount++;
|
|
const body = JSON.parse(opts.body as string);
|
|
|
|
// First call: check if it has delay parts and return 400
|
|
if (callCount === 1) {
|
|
expect(body).toHaveProperty("delay_id", "mock_delay_id");
|
|
return {
|
|
status: 400,
|
|
body: { errcode: "M_BAD_JSON", error: "Unsupported parameters" },
|
|
};
|
|
}
|
|
|
|
// Second call: check if delay parts were stripped and return success
|
|
expect(body).not.toHaveProperty("delay_id");
|
|
expect(body).not.toHaveProperty("delay_timeout");
|
|
expect(body).not.toHaveProperty("delay_cs_api_url");
|
|
|
|
return {
|
|
status: 200,
|
|
body: { url: serviceUrl, jwt: testJWTToken },
|
|
};
|
|
},
|
|
{ overwriteRoutes: true },
|
|
);
|
|
|
|
const config = await getSFUConfig({ ...params, ...delayParams });
|
|
|
|
expect(config.jwt).toBe(testJWTToken);
|
|
expect(callCount).toBe(2);
|
|
});
|
|
|
|
it("should successfully send delay parameters to the JWT service legacy endpoint", async () => {
|
|
fetchMock.post(
|
|
"https://sfu.example.org/sfu/get",
|
|
(url, opts) => {
|
|
const body = JSON.parse(opts.body as string);
|
|
|
|
// Verify, that the request contains the expected delay parameters
|
|
if (
|
|
body.delay_id === "mock_delay_id" &&
|
|
body.delay_timeout === 3600000 &&
|
|
body.delay_cs_api_url === "https://matrix.homeserverserver.org"
|
|
) {
|
|
return {
|
|
status: 200,
|
|
body: { url: serviceUrl, jwt: testJWTToken },
|
|
};
|
|
}
|
|
return {
|
|
status: 400,
|
|
body: { error: "Missing expected delay params" },
|
|
};
|
|
},
|
|
{ overwriteRoutes: true },
|
|
);
|
|
|
|
const config = await getSFUConfig({ ...params, ...delayParams });
|
|
|
|
expect(config).toMatchObject({
|
|
jwt: testJWTToken,
|
|
url: serviceUrl,
|
|
});
|
|
});
|
|
|
|
it("should use the default endpoint when subscribing", async () => {
|
|
fetchMock.post("https://sfu.example.org/get_token", (url, opts) => {
|
|
const body = JSON.parse(opts.body as string);
|
|
expect(body).toMatchObject({
|
|
room_id: "!example_room_id",
|
|
slot_id: "m.call#ROOM",
|
|
member: {
|
|
id: ownMemberMock.memberId,
|
|
claimed_user_id: ownMemberMock.userId,
|
|
claimed_device_id: ownMemberMock.deviceId,
|
|
},
|
|
});
|
|
return { status: 200, body: { url: serviceUrl, jwt: testJWTToken } };
|
|
});
|
|
const config = await getSFUConfig({ ...params, role: "subscriber" });
|
|
expect(config.jwt).toBe(testJWTToken);
|
|
expect(fetchMock.called("https://sfu.example.org/sfu/get")).toBe(false);
|
|
});
|
|
|
|
it("should fail when subscribing if the default endpoint is missing", async () => {
|
|
// Nothing but the catch-all 404 answers the default endpoint. Subscribers
|
|
// must not fall back to the legacy endpoint, which would hand them a
|
|
// legacy identity.
|
|
await expect(
|
|
getSFUConfig({ ...params, role: "subscriber" }),
|
|
).rejects.toThrow(FailToGetOpenIdToken);
|
|
expect(fetchMock.called("https://sfu.example.org/get_token")).toBe(true);
|
|
expect(fetchMock.called("https://sfu.example.org/sfu/get")).toBe(false);
|
|
});
|
|
|
|
it("should retry fetching the openid token", async () => {
|
|
let count = 0;
|
|
matrixClient.getOpenIdToken.mockImplementation(async () => {
|
|
count++;
|
|
if (count < 2) {
|
|
throw Error("Test failure");
|
|
}
|
|
return Promise.resolve({
|
|
token_type: "Bearer",
|
|
access_token: "foobar",
|
|
matrix_server_name: "example.org",
|
|
expires_in: 30,
|
|
});
|
|
});
|
|
fetchMock.post("https://sfu.example.org/sfu/get", () => {
|
|
return {
|
|
status: 200,
|
|
body: { url: serviceUrl, jwt: testJWTToken },
|
|
};
|
|
});
|
|
const config = await getSFUConfig({ ...params, ...delayParams });
|
|
expect(config).toEqual({
|
|
jwt: testJWTToken,
|
|
url: serviceUrl,
|
|
livekitIdentity: "@me:example.org:ABCDEF",
|
|
livekitAlias: "!example_room_id",
|
|
});
|
|
});
|
|
});
|