2023-07-05 13:12:37 +01:00
/*
2024-09-06 10:22:13 +02:00
Copyright 2023, 2024 New Vector Ltd.
2023-07-05 13:12:37 +01:00
2025-02-18 17:59:58 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-06 10:22:13 +02:00
Please see LICENSE in the repository root for full details.
2023-07-05 13:12:37 +01:00
*/
2026-01-07 17:21:08 +01:00
import {
retryNetworkOperation ,
type IOpenIDToken ,
type MatrixClient ,
} from "matrix-js-sdk" ;
2025-12-17 09:53:49 +01:00
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager" ;
2025-12-29 17:38:54 +01:00
import { type Logger } from "matrix-js-sdk/lib/logger" ;
2023-07-05 13:12:37 +01:00
2025-03-21 15:07:15 -04:00
import { FailToGetOpenIdToken } from "../utils/errors" ;
import { doNetworkOperationWithRetry } from "../utils/matrix" ;
2025-12-17 09:53:49 +01:00
import { Config } from "../config/Config" ;
2023-07-12 17:57:54 +01:00
2025-12-29 17:45:41 +00:00
/**
* Configuration and access tokens provided by the SFU on successful authentication.
*/
2023-07-05 13:12:37 +01:00
export interface SFUConfig {
url : string ;
jwt : string ;
2025-12-29 17:45:41 +00:00
livekitAlias : string ;
2026-01-05 21:58:26 +01:00
// NOTE: Currently unused.
2025-12-29 17:45:41 +00:00
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 ;
};
2023-07-05 13:12:37 +01:00
}
// The bits we need from MatrixClient
export type OpenIDClientParts = Pick <
MatrixClient ,
"getOpenIdToken" | "getDeviceId"
> ;
2025-11-20 14:42:12 +01:00
/**
2025-11-21 13:04:28 +01:00
* 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.
2025-12-30 17:02:44 +01:00
* @param client The Matrix client
2026-01-05 22:20:19 +01:00
* @param membership Our own membership identity parts used to send to jwt service.
2025-12-30 17:02:44 +01:00
* @param serviceUrl The URL of the livekit SFU service
2026-01-05 21:58:26 +01:00
* @param forceOldJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination
2025-12-29 17:38:54 +01:00
* instead of a hash.
* This function by default uses whatever is possible with the current jwt service installed next to the SFU.
* For remote connections this does not matter, since we will not publish there we can rely on the newest option.
* For our own connection we can only use the hashed version if we also send the new matrix2.0 sticky events.
2026-01-05 21:08:21 +01:00
* @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases.
2026-01-05 22:20:19 +01:00
* @param delayEndpointBaseUrl The URL of the matrix homeserver.
* @param delayId The delay id used for the jwt service to manage.
* @param logger optional logger.
2025-11-21 13:04:28 +01:00
* @returns Object containing the token information
2025-11-20 14:42:12 +01:00
* @throws FailToGetOpenIdToken
*/
2023-07-05 13:12:37 +01:00
export async function getSFUConfigWithOpenID (
client : OpenIDClientParts ,
2025-12-17 09:53:49 +01:00
membership : CallMembershipIdentityParts ,
2025-08-27 14:01:01 +02:00
serviceUrl : string ,
2025-12-29 17:38:54 +01:00
forceOldJwtEndpoint : boolean ,
2026-01-05 21:08:21 +01:00
roomId : string ,
2025-12-17 09:53:49 +01:00
delayEndpointBaseUrl? : string ,
delayId? : string ,
2025-12-29 17:38:54 +01:00
logger? : Logger ,
2025-08-27 14:01:01 +02:00
) : Promise < SFUConfig > {
2025-03-11 09:07:19 +01:00
let openIdToken : IOpenIDToken ;
try {
openIdToken = await doNetworkOperationWithRetry ( async () =>
client . getOpenIdToken (),
);
} catch ( error ) {
throw new FailToGetOpenIdToken (
error instanceof Error ? error : new Error ( "Unknown error" ),
);
}
2025-12-29 17:38:54 +01:00
logger ? . debug ( "Got openID token" , openIdToken );
2023-07-05 13:12:37 +01:00
2025-12-29 17:38:54 +01:00
logger ? . info ( `Trying to get JWT for focus ${ serviceUrl } ...` );
2026-01-05 21:08:21 +01:00
2026-01-07 15:36:32 +01:00
let sfuConfig : { url : string ; jwt : string } | undefined ;
2025-12-29 17:38:54 +01:00
2026-01-07 15:36:32 +01:00
// If forceOldJwtEndpoint is set we indicate that we do not want to try the new endpoint,
// since we are not sending the new matrix2.0 sticky events (no hashed identity in the event)
if ( forceOldJwtEndpoint === false ) {
try {
2026-01-07 17:38:29 +01:00
sfuConfig = await getLiveKitJWTWithDelayDelegation (
membership ,
serviceUrl ,
roomId ,
openIdToken ,
delayEndpointBaseUrl ,
delayId ,
);
logger ? . info ( `Got JWT from call's active focus URL.` );
2026-01-07 15:36:32 +01:00
} catch ( e ) {
2026-01-07 17:21:08 +01:00
if ( e instanceof NotSupportedError ) {
logger ? . warn (
`Failed fetching jwt with matrix 2.0 endpoint (retry with legacy) Not supported` ,
e ,
);
sfuConfig = undefined ;
} else {
logger ? . warn (
`Failed fetching jwt with matrix 2.0 endpoint other issues -> not going to try with legacy endpoint` ,
e ,
);
}
2026-01-07 15:36:32 +01:00
}
}
2026-01-07 17:21:08 +01:00
// DEPRECATED
2026-01-07 15:36:32 +01:00
// Either forceOldJwtEndpoint = true or getLiveKitJWTWithDelayDelegation throws -> reset sfuConfig = undefined
if ( sfuConfig === undefined ) {
2026-01-07 17:38:29 +01:00
sfuConfig = await getLiveKitJWT (
membership . deviceId ,
serviceUrl ,
roomId ,
openIdToken ,
);
2026-01-07 17:21:08 +01:00
logger ? . info ( `Got JWT from call's active focus URL.` );
}
if ( ! sfuConfig ) {
throw new Error ( "No `sfuConfig` after trying with old and new endpoints" );
2026-01-07 15:36:32 +01:00
}
// Pull the details from the JWT
2025-12-29 17:45:41 +00:00
const [, payloadStr ] = sfuConfig . jwt . split ( "." );
2026-01-05 21:58:26 +01:00
// TODO: Prefer Uint8Array.fromBase64 when widely available
2025-12-29 17:45:41 +00:00
const payload = JSON . parse ( global . atob ( payloadStr )) as SFUJWTPayload ;
return {
jwt : sfuConfig.jwt ,
url : sfuConfig.url ,
livekitAlias : payload.video.room ,
// NOTE: Currently unused.
2026-01-05 21:58:26 +01:00
// Probably also not helpful since we now compute the backendIdentity on joining the call so we can use it for the encryption manager.
// The only reason for us to know it locally is to connect the right users with the lk world. (and to set our own keys)
2025-12-29 17:45:41 +00:00
livekitIdentity : payload.sub ,
};
2023-07-12 17:57:54 +01:00
}
2026-01-07 17:38:29 +01:00
const RETRIES = 4 ;
2023-07-12 17:57:54 +01:00
async function getLiveKitJWT (
2026-01-05 21:58:26 +01:00
deviceId : string ,
2023-07-12 17:57:54 +01:00
livekitServiceURL : string ,
2026-01-05 21:08:21 +01:00
matrixRoomId : string ,
2023-10-11 10:42:04 -04:00
openIDToken : IOpenIDToken ,
2025-12-29 17:45:41 +00:00
) : Promise < { url : string ; jwt : string } > {
2026-01-07 17:38:29 +01:00
let res : Response | undefined ;
await retryNetworkOperation ( RETRIES , async () => {
res = await fetch ( livekitServiceURL + "/sfu/get" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
},
body : JSON.stringify ({
// This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used.
room : matrixRoomId ,
openid_token : openIDToken ,
device_id : deviceId ,
}),
});
2026-01-07 17:21:08 +01:00
});
2026-01-07 17:38:29 +01:00
if ( ! res ) {
throw new Error (
`Network error while connecting to jwt service after ${ RETRIES } retries` ,
);
}
2026-01-07 17:21:08 +01:00
if ( ! res . ok ) {
throw new Error ( "SFU Config fetch failed with status code " + res . status );
}
return await res . json ();
}
class NotSupportedError extends Error {
public constructor ( message : string ) {
super ( message );
this . name = "NotSupported" ;
2023-07-05 13:12:37 +01:00
}
}
2025-12-17 09:53:49 +01:00
export async function getLiveKitJWTWithDelayDelegation (
membership : CallMembershipIdentityParts ,
livekitServiceURL : string ,
2026-01-05 21:08:21 +01:00
matrixRoomId : string ,
2025-12-17 09:53:49 +01:00
openIDToken : IOpenIDToken ,
delayEndpointBaseUrl? : string ,
delayId? : string ,
2026-01-05 21:08:21 +01:00
) : Promise < { url : string ; jwt : string } > {
2025-12-17 09:53:49 +01:00
const { userId , deviceId , memberId } = membership ;
const body = {
2026-01-05 21:08:21 +01:00
room_id : matrixRoomId ,
2025-12-17 09:53:49 +01:00
slot_id : "m.call#ROOM" ,
openid_token : openIDToken ,
member : {
id : memberId ,
claimed_user_id : userId ,
claimed_device_id : deviceId ,
},
};
let bodyDalayParts = {};
// Also check for empty string
if ( delayId && delayEndpointBaseUrl ) {
const delayTimeoutMs =
Config . get (). matrix_rtc_session ? . delayed_leave_event_delay_ms ?? 1000 ;
bodyDalayParts = {
delay_id : delayId ,
delay_timeout : delayTimeoutMs ,
delay_cs_api_url : delayEndpointBaseUrl ,
};
}
2026-01-07 17:38:29 +01:00
let res : Response | undefined ;
await retryNetworkOperation ( RETRIES , async () => {
res = await fetch ( livekitServiceURL + "/get_token" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
},
body : JSON.stringify ({ ... body , ... bodyDalayParts }),
});
2026-01-07 17:21:08 +01:00
});
2026-01-07 17:38:29 +01:00
if ( ! res ) {
throw new Error (
`Network error while connecting to jwt service after ${ RETRIES } retries` ,
);
}
2026-01-07 17:21:08 +01:00
if ( ! res . ok ) {
const msg = "SFU Config fetch failed with status code " + res . status ;
if ( res . status === 404 ) {
throw new NotSupportedError ( msg );
} else {
throw new Error ( msg );
2025-12-17 09:53:49 +01:00
}
}
2026-01-07 17:21:08 +01:00
return await res . json ();
2025-12-17 09:53:49 +01:00
}