Skip to content

Commit

Permalink
Support for stable MSC3882 get_login_token (#3416)
Browse files Browse the repository at this point in the history
* Support for stable MSC3882 get_login_token

* Make changes non-breaking by deprecation

* Update src/@types/auth.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update spec/integ/matrix-client-methods.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Suggestions from review

* Update src/client.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Fix and test prefix behaviour

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
  • Loading branch information
3 people authored Sep 29, 2023
1 parent 74193ad commit f33da83
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 50 deletions.
40 changes: 31 additions & 9 deletions spec/integ/matrix-client-methods.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1204,51 +1204,73 @@ describe("MatrixClient", function () {

describe("requestLoginToken", () => {
it("should hit the expected API endpoint with UIA", async () => {
jest.spyOn(client.http, "getUrl");
httpBackend
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
.respond(200, { capabilities: { "m.get_login_token": { enabled: true } } });
const response = {};
const uiaData = {};
const prom = client.requestLoginToken(uiaData);
httpBackend
.when("POST", "/unstable/org.matrix.msc3882/login/get_token", { auth: uiaData })
.respond(200, response);
httpBackend.when("POST", "/v1/login/get_token", { auth: uiaData }).respond(200, response);
await httpBackend.flush("");
expect(await prom).toStrictEqual(response);
expect(client.http.getUrl).toHaveLastReturnedWith(
expect.objectContaining({
href: "http://alice.localhost.test.server/_matrix/client/v1/login/get_token",
}),
);
});

it("should hit the expected API endpoint without UIA", async () => {
jest.spyOn(client.http, "getUrl");
httpBackend
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
.respond(200, { capabilities: { "m.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);
httpBackend.when("POST", "/v1/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 });
expect(client.http.getUrl).toHaveLastReturnedWith(
expect.objectContaining({
href: "http://alice.localhost.test.server/_matrix/client/v1/login/get_token",
}),
);
});

it("should hit the r1 endpoint when capability is disabled", async () => {
it("should still hit the stable endpoint when capability is disabled (but present)", async () => {
jest.spyOn(client.http, "getUrl");
httpBackend
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: false } } });
.respond(200, { capabilities: { "m.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);
httpBackend.when("POST", "/v1/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 });
expect(client.http.getUrl).toHaveLastReturnedWith(
expect.objectContaining({
href: "http://alice.localhost.test.server/_matrix/client/v1/login/get_token",
}),
);
});

it("should hit the r0 endpoint for fallback", async () => {
jest.spyOn(client.http, "getUrl");
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("");
// check that expires_in has been populated for compatibility with r1
expect(await prom).toStrictEqual({ ...response, expires_in_ms: 5000 });
expect(client.http.getUrl).toHaveLastReturnedWith(
expect.objectContaining({
href: "http://alice.localhost.test.server/_matrix/client/unstable/org.matrix.msc3882/login/token",
}),
);
});
});

Expand Down
32 changes: 16 additions & 16 deletions spec/unit/rendezvous/rendezvous.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function makeMockClient(opts: {
userId: string;
deviceId: string;
deviceKey?: string;
msc3882Enabled: boolean;
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
msc3886Enabled: boolean;
devices?: Record<string, Partial<DeviceInfo>>;
Expand All @@ -54,7 +54,7 @@ function makeMockClient(opts: {
getVersions() {
return {
unstable_features: {
"org.matrix.msc3882": opts.msc3882Enabled,
"org.matrix.msc3882": opts.getLoginTokenEnabled,
"org.matrix.msc3886": opts.msc3886Enabled,
},
};
Expand All @@ -64,8 +64,8 @@ function makeMockClient(opts: {
? {}
: {
capabilities: {
"org.matrix.msc3882.get_login_token": {
enabled: opts.msc3882Enabled,
"m.get_login_token": {
enabled: opts.getLoginTokenEnabled,
},
},
};
Expand Down Expand Up @@ -122,7 +122,7 @@ describe("Rendezvous", function () {
userId: "@alice:example.com",
deviceId: "DEVICEID",
msc3886Enabled: false,
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: true,
});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
Expand Down Expand Up @@ -180,10 +180,10 @@ describe("Rendezvous", function () {
});

async function testNoProtocols({
msc3882Enabled,
getLoginTokenEnabled,
msc3882r0Only,
}: {
msc3882Enabled: boolean;
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
}) {
const aliceTransport = makeTransport("Alice");
Expand All @@ -198,7 +198,7 @@ describe("Rendezvous", function () {
userId: "alice",
deviceId: "ALICE",
msc3886Enabled: false,
msc3882Enabled,
getLoginTokenEnabled,
msc3882r0Only,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
Expand Down Expand Up @@ -241,11 +241,11 @@ describe("Rendezvous", function () {
}

it("no protocols - r0", async function () {
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: true });
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: true });
});

it("no protocols - r1", async function () {
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: false });
it("no protocols - stable", async function () {
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: false });
});

it("new device declines protocol with outcome unsupported", async function () {
Expand All @@ -260,7 +260,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
Expand Down Expand Up @@ -319,7 +319,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
Expand Down Expand Up @@ -378,7 +378,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
Expand Down Expand Up @@ -439,7 +439,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
Expand Down Expand Up @@ -508,7 +508,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
devices,
Expand Down
6 changes: 2 additions & 4 deletions src/@types/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,7 @@ export interface LoginResponse {
}

/**
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
* `m.login.token` issuance request.
* Note that this is UNSTABLE and subject to breaking changes without notice.
* The result of a successful `m.login.token` issuance request as per https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv1loginget_token
*/
export interface LoginTokenPostResponse {
/**
Expand All @@ -255,7 +253,7 @@ export interface LoginTokenPostResponse {
/**
* Expiration in seconds.
*
* @deprecated this is only provided for compatibility with original revision of the MSC.
* @deprecated this is only provided for compatibility with original revision of [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
*/
expires_in: number;
/**
Expand Down
47 changes: 35 additions & 12 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ import {
threadFilterTypeToFilter,
} from "./models/thread";
import { M_BEACON_INFO, MBeaconInfoEventContent } from "./@types/beacon";
import { UnstableValue } from "./NamespacedValue";
import { NamespacedValue, UnstableValue } from "./NamespacedValue";
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { IgnoredInvites } from "./models/invites-ignorer";
Expand Down Expand Up @@ -519,9 +519,22 @@ export interface IChangePasswordCapability extends ICapability {}

export interface IThreadsCapability extends ICapability {}

export interface IMSC3882GetLoginTokenCapability extends ICapability {}
export interface IGetLoginTokenCapability extends ICapability {}

export const UNSTABLE_MSC3882_CAPABILITY = new UnstableValue("m.get_login_token", "org.matrix.msc3882.get_login_token");
/**
* @deprecated use {@link IGetLoginTokenCapability} instead
*/
export type IMSC3882GetLoginTokenCapability = IGetLoginTokenCapability;

export const GET_LOGIN_TOKEN_CAPABILITY = new NamespacedValue(
"m.get_login_token",
"org.matrix.msc3882.get_login_token",
);

/**
* @deprecated use {@link GET_LOGIN_TOKEN_CAPABILITY} instead
*/
export const UNSTABLE_MSC3882_CAPABILITY = GET_LOGIN_TOKEN_CAPABILITY;

export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666";
export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
Expand All @@ -536,8 +549,8 @@ export interface Capabilities {
"m.change_password"?: IChangePasswordCapability;
"m.room_versions"?: IRoomVersionsCapability;
"io.element.thread"?: IThreadsCapability;
[UNSTABLE_MSC3882_CAPABILITY.name]?: IMSC3882GetLoginTokenCapability;
[UNSTABLE_MSC3882_CAPABILITY.altName]?: IMSC3882GetLoginTokenCapability;
"m.get_login_token"?: IGetLoginTokenCapability;
"org.matrix.msc3882.get_login_token"?: IGetLoginTokenCapability;
}

/** @deprecated prefer {@link CrossSigningKeyInfo}. */
Expand Down Expand Up @@ -8002,29 +8015,39 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Make a request for an `m.login.token` to be issued as per
* [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
* The server may require User-Interactive auth.
* Note that this is UNSTABLE and subject to breaking changes without notice.
*
* Compatibility with unstable implementations of MSC3882 is deprecated and will be removed in a future release.
*
* @param auth - Optional. Auth data to supply for User-Interactive auth.
* @returns Promise which resolves: On success, the token response
* or UIA auth data.
*/
public async requestLoginToken(auth?: AuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
// 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

let endpoint: string;
if (capabilities[GET_LOGIN_TOKEN_CAPABILITY.name]) {
// use the stable endpoint
endpoint = `${ClientPrefix.V1}/login/get_token`;
} else if (capabilities[GET_LOGIN_TOKEN_CAPABILITY.altName!]) {
// newer unstable r1 endpoint
endpoint = `${ClientPrefix.Unstable}/org.matrix.msc3882/login/get_token`;
} else {
// old unstable r0 endpoint
endpoint = `${ClientPrefix.Unstable}/org.matrix.msc3882/login/token`;
}

const body: UIARequest<{}> = { auth };
const res = await this.http.authedRequest<UIAResponse<LoginTokenPostResponse>>(
Method.Post,
endpoint,
undefined, // no query params
body,
{ prefix: ClientPrefix.Unstable },
{ prefix: "" },
);

// the representation of expires_in changed from revision 0 to revision 1 so we populate
// the representation of expires_in changed from unstable revision 0 to unstable revision 1 so we cross populate
if ("login_token" in res) {
if (typeof res.expires_in_ms === "number") {
res.expires_in = Math.floor(res.expires_in_ms / 1000);
Expand Down
13 changes: 4 additions & 9 deletions src/rendezvous/MSC3906Rendezvous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ limitations under the License.
import { UnstableValue } from "matrix-events-sdk";

import { RendezvousChannel, RendezvousFailureListener, RendezvousFailureReason, RendezvousIntent } from ".";
import {
ICrossSigningKey,
IMSC3882GetLoginTokenCapability,
MatrixClient,
UNSTABLE_MSC3882_CAPABILITY,
} from "../client";
import { ICrossSigningKey, IGetLoginTokenCapability, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "../client";
import { CrossSigningInfo } from "../crypto/CrossSigning";
import { DeviceInfo } from "../crypto/deviceinfo";
import { buildFeatureSupportMap, Feature, ServerSupport } from "../feature";
Expand Down Expand Up @@ -105,15 +100,15 @@ 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
// in stable and unstable r1 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<IMSC3882GetLoginTokenCapability>(capabilities);
const capability = GET_LOGIN_TOKEN_CAPABILITY.findIn<IGetLoginTokenCapability>(capabilities);

// determine available protocols
if (!capability?.enabled && features.get(Feature.LoginTokenRequest) === ServerSupport.Unsupported) {
logger.info("Server doesn't support MSC3882");
logger.info("Server doesn't support get_login_token");
await this.send({ type: PayloadType.Finish, outcome: Outcome.Unsupported });
await this.cancel(RendezvousFailureReason.HomeserverLacksSupport);
return undefined;
Expand Down

0 comments on commit f33da83

Please sign in to comment.