Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for stable MSC3882 get_login_token #3416

Merged
merged 8 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading