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
*/
2025-03-13 13:58:43 +01:00
import { 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 ;
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
2025-12-29 17:38:54 +01:00
* @param membership
2025-12-30 17:02:44 +01:00
* @param serviceUrl The URL of the livekit SFU service
2025-12-29 17:38:54 +01:00
* @param forceOldEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatination
* 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.
2025-12-29 17:38:54 +01:00
* @param delayEndpointBaseUrl
* @param delayId
* @param 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 } ...` );
2025-12-17 09:53:49 +01:00
const args : [ CallMembershipIdentityParts , string , string , IOpenIDToken ] = [
membership ,
2025-08-27 14:01:01 +02:00
serviceUrl ,
2026-01-05 21:08:21 +01:00
roomId ,
2025-08-27 14:01:01 +02:00
openIdToken ,
2025-12-17 09:53:49 +01:00
];
2026-01-05 21:08:21 +01:00
let sfuConfig : { url : string ; jwt : string };
2025-12-29 17:38:54 +01:00
try {
// we do not want to try the old endpoint, since we are not sending the new matrix2.0 sticky events (no hashed identity in the event)
if ( forceOldJwtEndpoint ) throw new Error ( "Force old jwt endpoint" );
if ( ! delayId )
throw new Error ( "No delayId, Won't try matrix 2.0 jwt endpoint." );
2026-01-05 21:08:21 +01:00
sfuConfig = await getLiveKitJWTWithDelayDelegation (
2025-12-17 09:53:49 +01:00
... args ,
delayEndpointBaseUrl ,
delayId ,
);
2025-12-29 17:38:54 +01:00
logger ? . info ( `Got JWT from call's active focus URL.` );
} catch ( e ) {
logger ? . warn (
`Failed fetching jwt with matrix 2.0 endpoint (retry with legacy)` ,
e ,
);
2026-01-05 21:08:21 +01:00
sfuConfig = await getLiveKitJWT (... args );
2025-12-29 17:38:54 +01:00
logger ? . info ( `Got JWT from call's active focus URL.` );
2026-01-05 21:08:21 +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:08:21 +01:00
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.
livekitIdentity : payload.sub ,
};
2023-07-12 17:57:54 +01:00
}
async function getLiveKitJWT (
2025-12-17 09:53:49 +01:00
membership : CallMembershipIdentityParts ,
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 } > {
2023-07-12 17:57:54 +01:00
try {
const res = await fetch ( livekitServiceURL + "/sfu/get" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
},
body : JSON.stringify ({
2026-01-05 21:08:21 +01:00
// This is the actual livekit room alias. For the legacy jwt endpoint simply the room id was used.
room : matrixRoomId ,
2023-07-12 17:57:54 +01:00
openid_token : openIDToken ,
2025-12-17 09:53:49 +01:00
device_id : membership.deviceId ,
2023-07-12 17:57:54 +01:00
}),
});
if ( ! res . ok ) {
throw new Error ( "SFU Config fetch failed with status code " + res . status );
}
return await res . json ();
} catch ( e ) {
2025-12-29 17:45:41 +00:00
throw new Error ( "SFU Config fetch failed with exception" , { cause : e });
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 ,
};
}
try {
const res = await fetch ( livekitServiceURL + "/get_token" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
},
body : JSON.stringify ({ ... body , ... bodyDalayParts }),
});
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 " + e );
}
}