diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index e8896c518ec..33b5e1ad9eb 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -1127,22 +1127,51 @@ describe("MatrixClient", function () { describe("requestLoginToken", () => { it("should hit the expected API endpoint with UIA", async () => { + httpBackend! + .when("GET", "/capabilities") + .respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } }); const response = {}; const uiaData = {}; const prom = client!.requestLoginToken(uiaData); httpBackend! - .when("POST", "/unstable/org.matrix.msc3882/login/token", { auth: uiaData }) + .when("POST", "/unstable/org.matrix.msc3882/login/get_token", { auth: uiaData }) .respond(200, response); await httpBackend!.flush(""); expect(await prom).toStrictEqual(response); }); it("should hit the expected API endpoint without UIA", async () => { - const response = {}; + httpBackend! + .when("GET", "/capabilities") + .respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } }); + const response = { login_token: "xyz", expires_in_ms: 5000 }; + const prom = client!.requestLoginToken(); + httpBackend!.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response); + await httpBackend!.flush(""); + // check that expires_in has been populated for compatibility with r0 + expect(await prom).toStrictEqual({ ...response, expires_in: 5 }); + }); + + it("should hit the r1 endpoint when capability is disabled", async () => { + httpBackend! + .when("GET", "/capabilities") + .respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: false } } }); + const response = { login_token: "xyz", expires_in_ms: 5000 }; + const prom = client!.requestLoginToken(); + httpBackend!.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response); + await httpBackend!.flush(""); + // check that expires_in has been populated for compatibility with r0 + expect(await prom).toStrictEqual({ ...response, expires_in: 5 }); + }); + + it("should hit the r0 endpoint for fallback", async () => { + httpBackend!.when("GET", "/capabilities").respond(200, {}); + const response = { login_token: "xyz", expires_in: 5 }; const prom = client!.requestLoginToken(); httpBackend!.when("POST", "/unstable/org.matrix.msc3882/login/token", {}).respond(200, response); await httpBackend!.flush(""); - expect(await prom).toStrictEqual(response); + // check that expires_in has been populated for compatibility with r1 + expect(await prom).toStrictEqual({ ...response, expires_in_ms: 5000 }); }); }); diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 59a3ac71613..087b2f2de7e 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -38,6 +38,7 @@ function makeMockClient(opts: { deviceId: string; deviceKey?: string; msc3882Enabled: boolean; + msc3882r0Only: boolean; msc3886Enabled: boolean; devices?: Record>; verificationFunction?: ( @@ -58,6 +59,17 @@ function makeMockClient(opts: { }, }; }, + getCapabilities() { + return opts.msc3882r0Only + ? {} + : { + capabilities: { + "org.matrix.msc3882.get_login_token": { + enabled: opts.msc3882Enabled, + }, + }, + }; + }, getUserId() { return opts.userId; }, @@ -111,6 +123,7 @@ describe("Rendezvous", function () { deviceId: "DEVICEID", msc3886Enabled: false, msc3882Enabled: true, + msc3882r0Only: true, }); httpBackend.when("POST", "https://fallbackserver/rz").response = { body: null, @@ -166,7 +179,13 @@ describe("Rendezvous", function () { await aliceRz.close(); }); - it("no protocols", async function () { + async function testNoProtocols({ + msc3882Enabled, + msc3882r0Only, + }: { + msc3882Enabled: boolean; + msc3882r0Only: boolean; + }) { const aliceTransport = makeTransport("Alice"); const bobTransport = makeTransport("Bob", "https://test.rz/999999"); transports.push(aliceTransport, bobTransport); @@ -178,8 +197,9 @@ describe("Rendezvous", function () { const alice = makeMockClient({ userId: "alice", deviceId: "ALICE", - msc3882Enabled: false, msc3886Enabled: false, + msc3882Enabled, + msc3882r0Only, }); const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice); @@ -218,6 +238,14 @@ describe("Rendezvous", function () { await aliceStartProm; await bobStartPromise; + } + + it("no protocols - r0", async function () { + await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: true }); + }); + + it("no protocols - r1", async function () { + await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: false }); }); it("new device declines protocol with outcome unsupported", async function () { @@ -233,6 +261,7 @@ describe("Rendezvous", function () { userId: "alice", deviceId: "ALICE", msc3882Enabled: true, + msc3882r0Only: false, msc3886Enabled: false, }); const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); @@ -291,6 +320,7 @@ describe("Rendezvous", function () { userId: "alice", deviceId: "ALICE", msc3882Enabled: true, + msc3882r0Only: false, msc3886Enabled: false, }); const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); @@ -349,6 +379,7 @@ describe("Rendezvous", function () { userId: "alice", deviceId: "ALICE", msc3882Enabled: true, + msc3882r0Only: false, msc3886Enabled: false, }); const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); @@ -409,6 +440,7 @@ describe("Rendezvous", function () { userId: "alice", deviceId: "ALICE", msc3882Enabled: true, + msc3882r0Only: false, msc3886Enabled: false, }); const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure); @@ -477,6 +509,7 @@ describe("Rendezvous", function () { userId: "alice", deviceId: "ALICE", msc3882Enabled: true, + msc3882r0Only: false, msc3886Enabled: false, devices, deviceKey: "aaaa", diff --git a/src/@types/auth.ts b/src/@types/auth.ts index 2b8f5d76f97..f34e45bf130 100644 --- a/src/@types/auth.ts +++ b/src/@types/auth.ts @@ -112,6 +112,12 @@ export interface LoginTokenPostResponse { login_token: string; /** * Expiration in seconds. + * + * @deprecated this is only provided for compatibility with original revision of the MSC. */ expires_in: number; + /** + * Expiration in milliseconds. + */ + expires_in_ms: number; } diff --git a/src/client.ts b/src/client.ts index fbb133c664c..9a5fc15bebe 100644 --- a/src/client.ts +++ b/src/client.ts @@ -489,11 +489,17 @@ export interface IChangePasswordCapability extends ICapability {} export interface IThreadsCapability extends ICapability {} +export interface IMSC3882GetLoginTokenCapability extends ICapability {} + +export const UNSTABLE_MSC3882_CAPABILITY = new UnstableValue("m.get_login_token", "org.matrix.msc3882.get_login_token"); + interface ICapabilities { [key: string]: any; "m.change_password"?: IChangePasswordCapability; "m.room_versions"?: IRoomVersionsCapability; "io.element.thread"?: IThreadsCapability; + [UNSTABLE_MSC3882_CAPABILITY.name]?: IMSC3882GetLoginTokenCapability; + [UNSTABLE_MSC3882_CAPABILITY.altName]?: IMSC3882GetLoginTokenCapability; } /* eslint-disable camelcase */ @@ -7809,15 +7815,33 @@ export class MatrixClient extends TypedEventEmitter> { + public async requestLoginToken(auth?: IAuthData): Promise> { + // use capabilities to determine which revision of the MSC is being used + const capabilities = await this.getCapabilities(); + // use r1 endpoint if capability is exposed otherwise use old r0 endpoint + const endpoint = UNSTABLE_MSC3882_CAPABILITY.findIn(capabilities) + ? "/org.matrix.msc3882/login/get_token" // r1 endpoint + : "/org.matrix.msc3882/login/token"; // r0 endpoint + const body: UIARequest<{}> = { auth }; - return this.http.authedRequest( + const res = await this.http.authedRequest>( Method.Post, - "/org.matrix.msc3882/login/token", + endpoint, undefined, // no query params body, { prefix: ClientPrefix.Unstable }, ); + + // the representation of expires_in changed from revision 0 to revision 1 so we populate + if ("login_token" in res) { + if (typeof res.expires_in_ms === "number") { + res.expires_in = Math.floor(res.expires_in_ms / 1000); + } else if (typeof res.expires_in === "number") { + res.expires_in_ms = res.expires_in * 1000; + } + } + + return res; } /** diff --git a/src/feature.ts b/src/feature.ts index 9141e811c49..9a0f1f92ad1 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -25,6 +25,9 @@ export enum ServerSupport { export enum Feature { Thread = "Thread", ThreadUnreadNotifications = "ThreadUnreadNotifications", + /** + * @deprecated this is now exposed as a capability not a feature + */ LoginTokenRequest = "LoginTokenRequest", RelationBasedRedactions = "RelationBasedRedactions", AccountDataDeletion = "AccountDataDeletion", diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index f431c8358d0..3528a66decf 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -17,7 +17,7 @@ limitations under the License. import { UnstableValue } from "matrix-events-sdk"; import { RendezvousChannel, RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from "."; -import { MatrixClient } from "../client"; +import { IMSC3882GetLoginTokenCapability, MatrixClient, UNSTABLE_MSC3882_CAPABILITY } from "../client"; import { CrossSigningInfo } from "../crypto/CrossSigning"; import { DeviceInfo } from "../crypto/deviceinfo"; import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature"; @@ -100,9 +100,14 @@ export class MSC3906Rendezvous { logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`); + // in r1 of MSC3882 the availability is exposed as a capability + const capabilities = await this.client.getCapabilities(); + // in r0 of MSC3882 the availability is exposed as a feature flag const features = await buildFeatureSupportMap(await this.client.getVersions()); + const capability = UNSTABLE_MSC3882_CAPABILITY.findIn(capabilities); + // determine available protocols - if (features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) { + if (!capability?.enabled && features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) { logger.info("Server doesn't support MSC3882"); await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported }); await this.cancel(RendezvousFailureReason.HomeserverLacksSupport);