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
2026-01-09 13:38:26 +01:00
import {
FailToGetOpenIdToken ,
NoMatrix2AuthorizationService ,
} from "../utils/errors" ;
2025-03-21 15:07:15 -04:00
import { doNetworkOperationWithRetry } from "../utils/matrix" ;
2025-12-17 09:53:49 +01:00
import { Config } from "../config/Config" ;
2026-01-09 13:38:26 +01:00
import { JwtEndpointVersion } from "../state/CallViewModel/localMember/LocalTransport" ;
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-09 13:38:26 +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-28 14:22:21 +01:00
* @param opts Additional options to modify which endpoint with which data will be used to acquire the jwt token .
* @param opts . forceJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatenation
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-09 13:38:26 +01:00
* @param opts . delayEndpointBaseUrl The URL of the matrix homeserver .
* @param opts . delayId The delay id used for the jwt service to manage .
2026-01-05 22:20:19 +01:00
* @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 ,
2026-01-05 21:08:21 +01:00
roomId : string ,
2026-01-09 13:38:26 +01:00
opts ? : {
forceJwtEndpoint? : JwtEndpointVersion ;
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-09 18:05:26 +01:00
const tryBothJwtEndpoints = opts ? . forceJwtEndpoint === undefined ; // This is for SFUs where we do not publish.
const forceMatrix2Jwt =
opts ? . forceJwtEndpoint === JwtEndpointVersion . Matrix_2_0 ;
// We want to start using the new endpoint (with optional delay delegation)
// if we can use both or if we are forced to use the new one.
if ( tryBothJwtEndpoints || forceMatrix2Jwt ) {
2026-01-07 15:36:32 +01:00
try {
2026-01-07 17:38:29 +01:00
sfuConfig = await getLiveKitJWTWithDelayDelegation (
membership ,
serviceUrl ,
roomId ,
openIdToken ,
2026-01-09 13:38:26 +01:00
opts ? . delayEndpointBaseUrl ,
opts ? . delayId ,
2026-01-07 17:38:29 +01:00
) ;
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 (
2026-01-08 12:27:17 +01:00
` Failed fetching jwt with matrix 2.0 endpoint other issues -> ` ,
2026-01-26 15:43:07 +01:00
` (not going to try with legacy endpoint if forceMatrix2Jwt is set to false (it is ${ forceMatrix2Jwt } ), we did not get a not supported error from the sfu) ` ,
2026-01-07 17:21:08 +01:00
e ,
) ;
2026-01-09 13:38:26 +01:00
// Make this throw a hard error in case we force the matrix2.0 endpoint.
2026-01-09 18:05:26 +01:00
if ( forceMatrix2Jwt )
2026-01-09 13:38:26 +01:00
throw new NoMatrix2AuthorizationService ( e as Error ) ;
2026-01-09 18:05:26 +01:00
// NEVER get bejond this point if we forceMatrix2 and it failed!
2026-01-07 17:21:08 +01:00
}
2026-01-07 15:36:32 +01:00
}
}
2026-01-07 17:21:08 +01:00
// DEPRECATED
2026-01-09 18:05:26 +01:00
// here we either have a sfuConfig or we alredy exited because of `if (forceMatrix2) throw ...`
// The only case we can get into this condition is, if `forceMatrix2` is `false`
2026-01-07 15:36:32 +01:00
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
2026-01-09 13:38:26 +01:00
if ( delayId && delayEndpointBaseUrl ) {
2025-12-17 09:53:49 +01:00
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
}