From 78788406daeca2b1de87113ee2f737cdd06f23b5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2024 09:50:44 +0100 Subject: [PATCH 1/4] Remove abandoned MSC3886, MSC3903, MSC3906 implementations Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/auth/LoginWithQR-types.ts | 4 - src/components/views/auth/LoginWithQR.tsx | 161 ++------- src/components/views/auth/LoginWithQRFlow.tsx | 67 +--- .../settings/devices/LoginWithQRSection.tsx | 24 +- .../settings/tabs/user/SessionManagerTab.tsx | 9 +- .../settings/devices/LoginWithQR-test.tsx | 271 +--------------- .../devices/LoginWithQRSection-test.tsx | 69 +--- .../LoginWithQRSection-test.tsx.snap | 307 ------------------ .../tabs/user/SessionManagerTab-test.tsx | 40 --- 9 files changed, 32 insertions(+), 920 deletions(-) delete mode 100644 test/unit-tests/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap diff --git a/src/components/views/auth/LoginWithQR-types.ts b/src/components/views/auth/LoginWithQR-types.ts index 2b594d52ea5..2f041a7ced4 100644 --- a/src/components/views/auth/LoginWithQR-types.ts +++ b/src/components/views/auth/LoginWithQR-types.ts @@ -24,10 +24,6 @@ export enum Phase { WaitingForDevice, Verifying, Error, - /** - * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. - */ - LegacyConnected, } export enum Click { diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 81ca41a9b5c..e931b40a71d 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -9,11 +9,6 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { ClientRendezvousFailureReason, - LegacyRendezvousFailureReason, - MSC3886SimpleHttpRendezvousTransport, - MSC3903ECDHPayload, - MSC3903ECDHv2RendezvousChannel, - MSC3906Rendezvous, MSC4108FailureReason, MSC4108RendezvousSession, MSC4108SecureChannel, @@ -23,29 +18,21 @@ import { RendezvousIntent, } from "matrix-js-sdk/src/rendezvous"; import { logger } from "matrix-js-sdk/src/logger"; -import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { Click, Mode, Phase } from "./LoginWithQR-types"; import LoginWithQRFlow from "./LoginWithQRFlow"; -import { wrapRequestWithDialog } from "../../../utils/UserInteractiveAuth"; -import { _t } from "../../../languageHandler"; interface IProps { client: MatrixClient; mode: Mode; - legacy: boolean; onFinished(...args: any): void; } interface IState { phase: Phase; - rendezvous?: MSC3906Rendezvous | MSC4108SignInWithQR; + rendezvous?: MSC4108SignInWithQR; mediaPermissionError?: boolean; - - // MSC3906 - confirmationDigits?: string; - - // MSC4108 verificationUri?: string; userCode?: string; checkCode?: string; @@ -54,25 +41,18 @@ interface IState { } export enum LoginWithQRFailureReason { - /** - * @deprecated the MSC3906 implementation is deprecated in favour of MSC4108. - */ RateLimited = "rate_limited", CheckCodeMismatch = "check_code_mismatch", } export type FailureReason = RendezvousFailureReason | LoginWithQRFailureReason; -// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed. -// However, we want to keep this implementation around for some time. -// TODO: define an end-of-life date for this implementation. - /** * A component that allows sign in and E2EE set up with a QR code. * * It implements `login.reciprocate` capabilities and showing QR codes. * - * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 + * This uses the unstable feature of MSC4108: https://github.com/matrix-org/matrix-spec-proposals/pull/4108 */ export default class LoginWithQR extends React.Component { private finished = false; @@ -104,9 +84,6 @@ export default class LoginWithQR extends React.Component { if (this.state.rendezvous) { const rendezvous = this.state.rendezvous; rendezvous.onFailure = undefined; - if (rendezvous instanceof MSC3906Rendezvous) { - await rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled); - } this.setState({ rendezvous: undefined }); } if (mode === Mode.Show) { @@ -119,60 +96,7 @@ export default class LoginWithQR extends React.Component { // eslint-disable-next-line react/no-direct-mutation-state this.state.rendezvous.onFailure = undefined; // calling cancel will call close() as well to clean up the resources - if (this.state.rendezvous instanceof MSC3906Rendezvous) { - this.state.rendezvous.cancel(LegacyRendezvousFailureReason.UserCancelled); - } else { - this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled); - } - } - } - - private async legacyApproveLogin(): Promise { - if (!(this.state.rendezvous instanceof MSC3906Rendezvous)) { - throw new Error("Rendezvous not found"); - } - if (!this.props.client) { - throw new Error("No client to approve login with"); - } - this.setState({ phase: Phase.Loading }); - - try { - logger.info("Requesting login token"); - - const { login_token: loginToken } = await wrapRequestWithDialog(this.props.client.requestLoginToken, { - matrixClient: this.props.client, - title: _t("auth|qr_code_login|sign_in_new_device"), - })(); - - this.setState({ phase: Phase.WaitingForDevice }); - - const newDeviceId = await this.state.rendezvous.approveLoginOnExistingDevice(loginToken); - if (!newDeviceId) { - // user denied - return; - } - if (!this.props.client.getCrypto()) { - // no E2EE to set up - this.onFinished(true); - return; - } - this.setState({ phase: Phase.Verifying }); - await this.state.rendezvous.verifyNewDeviceOnExistingDevice(); - // clean up our state: - try { - await this.state.rendezvous.close(); - } finally { - this.setState({ rendezvous: undefined }); - } - this.onFinished(true); - } catch (e) { - logger.error("Error whilst approving sign in", e); - if (e instanceof HTTPError && e.httpStatus === 429) { - // 429: rate limit - this.setState({ phase: Phase.Error, failureReason: LoginWithQRFailureReason.RateLimited }); - return; - } - this.setState({ phase: Phase.Error, failureReason: ClientRendezvousFailureReason.Unknown }); + this.state.rendezvous.cancel(MSC4108FailureReason.UserCancelled); } } @@ -182,28 +106,18 @@ export default class LoginWithQR extends React.Component { } private generateAndShowCode = async (): Promise => { - let rendezvous: MSC4108SignInWithQR | MSC3906Rendezvous; + let rendezvous: MSC4108SignInWithQR; try { const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server; - if (this.props.legacy) { - const transport = new MSC3886SimpleHttpRendezvousTransport({ - onFailure: this.onFailure, - client: this.props.client, - fallbackRzServer, - }); - const channel = new MSC3903ECDHv2RendezvousChannel(transport, undefined, this.onFailure); - rendezvous = new MSC3906Rendezvous(channel, this.props.client, this.onFailure); - } else { - const transport = new MSC4108RendezvousSession({ - onFailure: this.onFailure, - client: this.props.client, - fallbackRzServer, - }); - await transport.send(""); - const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); - rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); - } + const transport = new MSC4108RendezvousSession({ + onFailure: this.onFailure, + client: this.props.client, + fallbackRzServer, + }); + await transport.send(""); + const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); + rendezvous = new MSC4108SignInWithQR(channel, false, this.props.client, this.onFailure); await rendezvous.generateCode(); this.setState({ @@ -218,10 +132,7 @@ export default class LoginWithQR extends React.Component { } try { - if (rendezvous instanceof MSC3906Rendezvous) { - const confirmationDigits = await rendezvous.startAfterShowingCode(); - this.setState({ phase: Phase.LegacyConnected, confirmationDigits }); - } else if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { + if (this.ourIntent === RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE) { // MSC4108-Flow: NewScanned await rendezvous.negotiateProtocols(); const { verificationUri } = await rendezvous.deviceAuthorizationGrant(); @@ -234,18 +145,9 @@ export default class LoginWithQR extends React.Component { // we ask the user to confirm that the channel is secure } catch (e: RendezvousError | unknown) { logger.error("Error whilst approving login", e); - if (rendezvous instanceof MSC3906Rendezvous) { - // only set to error phase if it hasn't already been set by onFailure or similar - if (this.state.phase !== Phase.Error) { - this.setState({ phase: Phase.Error, failureReason: LegacyRendezvousFailureReason.Unknown }); - } - } else { - await rendezvous?.cancel( - e instanceof RendezvousError - ? (e.code as MSC4108FailureReason) - : ClientRendezvousFailureReason.Unknown, - ); - } + await rendezvous?.cancel( + e instanceof RendezvousError ? (e.code as MSC4108FailureReason) : ClientRendezvousFailureReason.Unknown, + ); } }; @@ -298,7 +200,6 @@ export default class LoginWithQR extends React.Component { public reset(): void { this.setState({ rendezvous: undefined, - confirmationDigits: undefined, verificationUri: undefined, failureReason: undefined, userCode: undefined, @@ -311,16 +212,12 @@ export default class LoginWithQR extends React.Component { private onClick = async (type: Click, checkCode?: string): Promise => { switch (type) { case Click.Cancel: - if (this.state.rendezvous instanceof MSC3906Rendezvous) { - await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled); - } else { - await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); - } + await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); this.reset(); this.onFinished(false); break; case Click.Approve: - await (this.props.legacy ? this.legacyApproveLogin() : this.approveLogin(checkCode)); + await this.approveLogin(checkCode); break; case Click.Decline: await this.state.rendezvous?.declineLoginOnExistingDevice(); @@ -328,11 +225,7 @@ export default class LoginWithQR extends React.Component { this.onFinished(false); break; case Click.Back: - if (this.state.rendezvous instanceof MSC3906Rendezvous) { - await this.state.rendezvous?.cancel(LegacyRendezvousFailureReason.UserCancelled); - } else { - await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); - } + await this.state.rendezvous?.cancel(MSC4108FailureReason.UserCancelled); this.onFinished(false); break; case Click.ShowQr: @@ -342,20 +235,6 @@ export default class LoginWithQR extends React.Component { }; public render(): React.ReactNode { - if (this.state.rendezvous instanceof MSC3906Rendezvous) { - return ( - - ); - } - return ( { - code?: string; - confirmationDigits?: string; -} - interface Props { phase: Phase; code?: Uint8Array; @@ -47,19 +33,15 @@ interface Props { checkCode?: string; } -// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed. -// However, we want to keep this implementation around for some time. -// TODO: define an end-of-life date for this implementation. - /** * A component that implements the UI for sign in and E2EE set up with a QR code. * - * This supports the unstable features of MSC3906 and MSC4108 + * This supports the unstable features of MSC4108 */ -export default class LoginWithQRFlow extends React.Component> { +export default class LoginWithQRFlow extends React.Component { private checkCodeInput = createRef(); - public constructor(props: XOR) { + public constructor(props: Props) { super(props); } @@ -104,20 +86,17 @@ export default class LoginWithQRFlow extends React.Component -

{_t("auth|qr_code_login|confirm_code_match")}

-
{this.props.confirmationDigits}
-
-
- -
-
{_t("auth|qr_code_login|approve_access_warning")}
-
- - ); - - buttons = ( - <> - - {_t("action|approve")} - - - {_t("action|cancel")} - - - ); - break; case Phase.OutOfBandConfirmation: backButton = false; main = ( @@ -288,8 +232,7 @@ export default class LoginWithQRFlow extends React.Component diff --git a/src/components/views/settings/devices/LoginWithQRSection.tsx b/src/components/views/settings/devices/LoginWithQRSection.tsx index c5efb35efcf..e9d8029987c 100644 --- a/src/components/views/settings/devices/LoginWithQRSection.tsx +++ b/src/components/views/settings/devices/LoginWithQRSection.tsx @@ -8,10 +8,7 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { - IGetLoginTokenCapability, IServerVersions, - GET_LOGIN_TOKEN_CAPABILITY, - Capabilities, IClientWellKnown, OidcClientConfig, MatrixClient, @@ -28,27 +25,11 @@ import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext interface IProps { onShowQr: () => void; versions?: IServerVersions; - capabilities?: Capabilities; wellKnown?: IClientWellKnown; oidcClientConfig?: OidcClientConfig; isCrossSigningReady?: boolean; } -function shouldShowQrLegacy( - versions?: IServerVersions, - wellKnown?: IClientWellKnown, - capabilities?: Capabilities, -): boolean { - // Needs server support for (get_login_token or OIDC Device Authorization Grant) and MSC3886: - // in r0 of MSC3882 it is exposed as a feature flag, but in stable and unstable r1 it is a capability - const loginTokenCapability = GET_LOGIN_TOKEN_CAPABILITY.findIn(capabilities); - const getLoginTokenSupported = - !!versions?.unstable_features?.["org.matrix.msc3882"] || !!loginTokenCapability?.enabled; - const msc3886Supported = - !!versions?.unstable_features?.["org.matrix.msc3886"] || !!wellKnown?.["io.element.rendezvous"]?.server; - return getLoginTokenSupported && msc3886Supported; -} - export function shouldShowQr( cli: MatrixClient, isCrossSigningReady: boolean, @@ -73,15 +54,12 @@ export function shouldShowQr( const LoginWithQRSection: React.FC = ({ onShowQr, versions, - capabilities, wellKnown, oidcClientConfig, isCrossSigningReady, }) => { const cli = useMatrixClientContext(); - const offerShowQr = oidcClientConfig - ? shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown) - : shouldShowQrLegacy(versions, wellKnown, capabilities); + const offerShowQr = shouldShowQr(cli, !!isCrossSigningReady, oidcClientConfig, versions, wellKnown); return ( diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 783e9d350e5..3e74f04e76d 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -181,7 +181,6 @@ const SessionManagerTab: React.FC<{ const userId = matrixClient?.getUserId(); const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined; const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]); - const capabilities = useAsyncMemo(async () => matrixClient?.getCapabilities(), [matrixClient]); const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]); const oidcClientConfig = useAsyncMemo(async () => { try { @@ -292,12 +291,7 @@ const SessionManagerTab: React.FC<{ if (signInWithQrMode) { return ( }> - + ); } @@ -308,7 +302,6 @@ const SessionManagerTab: React.FC<{ ", () => { mode: Mode.Show, onFinished: jest.fn(), }; - const mockConfirmationDigits = "mock-confirmation-digits"; - const mockRendezvousCode = "mock-rendezvous-code"; - const newDeviceId = "new-device-id"; beforeEach(() => { mockedFlow.mockReset(); @@ -82,264 +73,10 @@ describe("", () => { cleanup(); }); - describe("MSC3906", () => { - const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => ( - - - - ); - - beforeEach(() => { - jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockResolvedValue(); - // @ts-ignore - // workaround for https://github.com/facebook/jest/issues/9675 - MSC3906Rendezvous.prototype.code = mockRendezvousCode; - jest.spyOn(MSC3906Rendezvous.prototype, "cancel").mockResolvedValue(); - jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockResolvedValue(mockConfirmationDigits); - jest.spyOn(MSC3906Rendezvous.prototype, "declineLoginOnExistingDevice").mockResolvedValue(); - jest.spyOn(MSC3906Rendezvous.prototype, "approveLoginOnExistingDevice").mockResolvedValue(newDeviceId); - jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockResolvedValue(undefined); - client.requestLoginToken.mockResolvedValue({ - login_token: "token", - expires_in_ms: 1000 * 1000, - } as LoginTokenPostResponse); // we force the type here so that it works with versions of js-sdk that don't have r1 support yet - }); - - test("no homeserver support", async () => { - // simulate no support - jest.spyOn(MSC3906Rendezvous.prototype, "generateCode").mockRejectedValue(""); - render(getComponent({ client })); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.Error, - failureReason: LegacyRendezvousFailureReason.HomeserverLacksSupport, - onClick: expect.any(Function), - }), - ); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - expect(rendezvous.generateCode).toHaveBeenCalled(); - }); - - test("failed to connect", async () => { - jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockRejectedValue(""); - render(getComponent({ client })); - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.Error, - failureReason: ClientRendezvousFailureReason.Unknown, - onClick: expect.any(Function), - }), - ); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - }); - - test("render QR then back", async () => { - const onFinished = jest.fn(); - jest.spyOn(MSC3906Rendezvous.prototype, "startAfterShowingCode").mockReturnValue(unresolvedPromise()); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.ShowingQR, - }), - ), - ); - // display QR code - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.ShowingQR, - code: mockRendezvousCode, - onClick: expect.any(Function), - }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - - // back - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Back); - expect(onFinished).toHaveBeenCalledWith(false); - expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled); - }); - - test("render QR then decline", async () => { - const onFinished = jest.fn(); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, - }), - ), - ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), - }); - - // decline - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Decline); - expect(onFinished).toHaveBeenCalledWith(false); - - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled(); - }); - - test("approve - no crypto", async () => { - (client as any).getCrypto = () => undefined; - const onFinished = jest.fn(); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, - }), - ), - ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), - }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - - // approve - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Approve); - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.WaitingForDevice, - }), - ), - ); - - expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token"); - - expect(onFinished).toHaveBeenCalledWith(true); - }); - - test("approve + verifying", async () => { - const onFinished = jest.fn(); - jest.spyOn(MSC3906Rendezvous.prototype, "verifyNewDeviceOnExistingDevice").mockImplementation(() => - unresolvedPromise(), - ); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, - }), - ), - ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), - }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - - // approve - const onClick = mockedFlow.mock.calls[0][0].onClick; - onClick(Click.Approve); - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.Verifying, - }), - ), - ); - - expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token"); - expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled(); - // expect(onFinished).toHaveBeenCalledWith(true); - }); - - test("approve + verify", async () => { - const onFinished = jest.fn(); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, - }), - ), - ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), - }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - - // approve - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Approve); - expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith("token"); - expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled(); - expect(rendezvous.close).toHaveBeenCalled(); - expect(onFinished).toHaveBeenCalledWith(true); - }); - - test("approve - rate limited", async () => { - mocked(client.requestLoginToken).mockRejectedValue(new HTTPError("rate limit reached", 429)); - const onFinished = jest.fn(); - render(getComponent({ client, onFinished })); - const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0]; - - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.LegacyConnected, - }), - ), - ); - expect(mockedFlow).toHaveBeenLastCalledWith({ - phase: Phase.LegacyConnected, - confirmationDigits: mockConfirmationDigits, - onClick: expect.any(Function), - }); - expect(rendezvous.generateCode).toHaveBeenCalled(); - expect(rendezvous.startAfterShowingCode).toHaveBeenCalled(); - - // approve - const onClick = mockedFlow.mock.calls[0][0].onClick; - await onClick(Click.Approve); - - // the 429 error should be handled and mapped - await waitFor(() => - expect(mockedFlow).toHaveBeenLastCalledWith( - expect.objectContaining({ - phase: Phase.Error, - failureReason: "rate_limited", - }), - ), - ); - }); - }); - describe("MSC4108", () => { const getComponent = (props: { client: MatrixClient; onFinished?: () => void }) => ( - + ); @@ -363,7 +100,7 @@ describe("", () => { const onClick = mockedFlow.mock.calls[0][0].onClick; await onClick(Click.Back); expect(onFinished).toHaveBeenCalledWith(false); - expect(rendezvous.cancel).toHaveBeenCalledWith(LegacyRendezvousFailureReason.UserCancelled); + expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UserCancelled); }); test("failed to connect", async () => { diff --git a/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx b/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx index fc06a88b6a7..dd6caef1ce1 100644 --- a/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx +++ b/test/unit-tests/components/views/settings/devices/LoginWithQRSection-test.tsx @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { render } from "jest-matrix-react"; import { mocked } from "jest-mock"; -import { IClientWellKnown, IServerVersions, MatrixClient, GET_LOGIN_TOKEN_CAPABILITY } from "matrix-js-sdk/src/matrix"; +import { IClientWellKnown, IServerVersions, MatrixClient } from "matrix-js-sdk/src/matrix"; import React from "react"; import fetchMock from "fetch-mock-jest"; @@ -51,73 +51,6 @@ describe("", () => { jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient({})); }); - describe("MSC3906", () => { - const defaultProps = { - onShowQr: () => {}, - versions: makeVersions({}), - wellKnown: {}, - }; - - const getComponent = (props = {}) => ; - - describe("should not render", () => { - it("no support at all", () => { - const { container } = render(getComponent()); - expect(container).toMatchSnapshot(); - }); - - it("only get_login_token enabled", async () => { - const { container } = render( - getComponent({ capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } } }), - ); - expect(container).toMatchSnapshot(); - }); - - it("MSC3886 + get_login_token disabled", async () => { - const { container } = render( - getComponent({ - versions: makeVersions({ "org.matrix.msc3886": true }), - capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: false } }, - }), - ); - expect(container).toMatchSnapshot(); - }); - }); - - describe("should render panel", () => { - it("get_login_token + MSC3886", async () => { - const { container } = render( - getComponent({ - versions: makeVersions({ - "org.matrix.msc3886": true, - }), - capabilities: { - [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true }, - }, - }), - ); - expect(container).toMatchSnapshot(); - }); - - it("get_login_token + .well-known", async () => { - const wellKnown = { - "io.element.rendezvous": { - server: "https://rz.local", - }, - }; - jest.spyOn(MatrixClientPeg, "get").mockReturnValue(makeClient(wellKnown)); - const { container } = render( - getComponent({ - versions: makeVersions({}), - capabilities: { [GET_LOGIN_TOKEN_CAPABILITY.name]: { enabled: true } }, - wellKnown, - }), - ); - expect(container).toMatchSnapshot(); - }); - }); - }); - describe("MSC4108", () => { describe("MSC4108", () => { const defaultProps = { diff --git a/test/unit-tests/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap b/test/unit-tests/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap deleted file mode 100644 index b84129e3a52..00000000000 --- a/test/unit-tests/components/views/settings/devices/__snapshots__/LoginWithQRSection-test.tsx.snap +++ /dev/null @@ -1,307 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` MSC3906 should not render MSC3886 + get_login_token disabled 1`] = ` -
-
-
-

- Link new device -

-
-
-
-

- Use a QR code to sign in to another device and set up secure messaging. -

-
- - - - - - Show QR code -
-

- Not supported by your account provider -

-
-
-
-
-`; - -exports[` MSC3906 should not render no support at all 1`] = ` -
-
-
-

- Link new device -

-
-
-
-

- Use a QR code to sign in to another device and set up secure messaging. -

-
- - - - - - Show QR code -
-

- Not supported by your account provider -

-
-
-
-
-`; - -exports[` MSC3906 should not render only get_login_token enabled 1`] = ` -
-
-
-

- Link new device -

-
-
-
-

- Use a QR code to sign in to another device and set up secure messaging. -

-
- - - - - - Show QR code -
-

- Not supported by your account provider -

-
-
-
-
-`; - -exports[` MSC3906 should render panel get_login_token + .well-known 1`] = ` -
-
-
-

- Link new device -

-
-
-
-

- Use a QR code to sign in to another device and set up secure messaging. -

-
- - - - - - Show QR code -
-
-
-
-
-`; - -exports[` MSC3906 should render panel get_login_token + MSC3886 1`] = ` -
-
-
-

- Link new device -

-
-
-
-

- Use a QR code to sign in to another device and set up secure messaging. -

-
- - - - - - Show QR code -
-
-
-
-
-`; diff --git a/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx index 8f0915a8303..32497cb0c88 100644 --- a/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -1650,46 +1650,6 @@ describe("", () => { expect(checkbox.getAttribute("aria-checked")).toEqual("false"); }); - describe("MSC3906 QR code login", () => { - const settingsValueSpy = jest.spyOn(SettingsStore, "getValue"); - - beforeEach(() => { - settingsValueSpy.mockClear().mockReturnValue(false); - // enable server support for qr login - mockClient.getVersions.mockResolvedValue({ - versions: [], - unstable_features: { - "org.matrix.msc3886": true, - }, - }); - mockClient.getCapabilities.mockResolvedValue({ - [GET_LOGIN_TOKEN_CAPABILITY.name]: { - enabled: true, - }, - }); - }); - - it("renders qr code login section", async () => { - const { getByText } = render(getComponent()); - - // wait for versions call to settle - await flushPromises(); - - expect(getByText("Link new device")).toBeTruthy(); - expect(getByText("Show QR code")).toBeTruthy(); - }); - - it("enters qr code login section when show QR code button clicked", async () => { - const { getByText, findByTestId } = render(getComponent()); - // wait for versions call to settle - await flushPromises(); - - fireEvent.click(getByText("Show QR code")); - - await expect(findByTestId("login-with-qr")).resolves.toBeTruthy(); - }); - }); - describe("MSC4108 QR code login", () => { const settingsValueSpy = jest.spyOn(SettingsStore, "getValue"); const issuer = "https://issuer.org"; From 65e161c4df4483acd942107465468e1fc65ddd5b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2024 10:01:50 +0100 Subject: [PATCH 2/4] Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 3 -- src/utils/UserInteractiveAuth.ts | 47 ------------------- .../settings/devices/LoginWithQRFlow-test.tsx | 26 ++-------- 3 files changed, 5 insertions(+), 71 deletions(-) delete mode 100644 src/utils/UserInteractiveAuth.ts diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7e96be0589a..7ba141c784d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -250,13 +250,11 @@ "phone_label": "Phone", "phone_optional_label": "Phone (optional)", "qr_code_login": { - "approve_access_warning": "By approving access for this device, it will have full access to your account.", "check_code_explainer": "This will verify that the connection to your other device is secure.", "check_code_heading": "Enter the number shown on your other device", "check_code_input_label": "2-digit code", "check_code_mismatch": "The numbers don't match", "completing_setup": "Completing set up of your new device", - "confirm_code_match": "Check that the code below matches with your other device:", "error_etag_missing": "An unexpected error occurred. This may be due to a browser extension, proxy server, or server misconfiguration.", "error_expired": "Sign in expired. Please try again.", "error_expired_title": "The sign in was not completed in time", @@ -284,7 +282,6 @@ "security_code": "Security code", "security_code_prompt": "If asked, enter the code below on your other device.", "select_qr_code": "Select \"%(scanQRCode)s\"", - "sign_in_new_device": "Sign in new device", "unsupported_explainer": "Your account provider doesn't support signing into a new device with a QR code.", "unsupported_heading": "QR code not supported", "waiting_for_device": "Waiting for device to sign in" diff --git a/src/utils/UserInteractiveAuth.ts b/src/utils/UserInteractiveAuth.ts deleted file mode 100644 index 96efc82f4ba..00000000000 --- a/src/utils/UserInteractiveAuth.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { AuthDict } from "matrix-js-sdk/src/interactive-auth"; -import { UIAResponse } from "matrix-js-sdk/src/matrix"; - -import Modal from "../Modal"; -import InteractiveAuthDialog, { InteractiveAuthDialogProps } from "../components/views/dialogs/InteractiveAuthDialog"; - -type FunctionWithUIA = (auth?: AuthDict, ...args: A[]) => Promise>; - -export function wrapRequestWithDialog( - requestFunction: FunctionWithUIA, - opts: Omit, "makeRequest" | "onFinished">, -): (...args: A[]) => Promise { - return async function (...args): Promise { - return new Promise((resolve, reject) => { - const boundFunction = requestFunction.bind(opts.matrixClient) as FunctionWithUIA; - boundFunction(undefined, ...args) - .then((res) => resolve(res as R)) - .catch((error) => { - if (error.httpStatus !== 401 || !error.data?.flows) { - // doesn't look like an interactive-auth failure - return reject(error); - } - - Modal.createDialog(InteractiveAuthDialog, { - ...opts, - authData: error.data, - makeRequest: (authData: AuthDict) => boundFunction(authData, ...args), - onFinished: (success, result) => { - if (success) { - resolve(result as R); - } else { - reject(result); - } - }, - }); - }); - }); - }; -} diff --git a/test/unit-tests/components/views/settings/devices/LoginWithQRFlow-test.tsx b/test/unit-tests/components/views/settings/devices/LoginWithQRFlow-test.tsx index 423a6298256..2a16c793a2c 100644 --- a/test/unit-tests/components/views/settings/devices/LoginWithQRFlow-test.tsx +++ b/test/unit-tests/components/views/settings/devices/LoginWithQRFlow-test.tsx @@ -8,11 +8,7 @@ Please see LICENSE files in the repository root for full details. import { cleanup, fireEvent, render, screen, waitFor } from "jest-matrix-react"; import React from "react"; -import { - ClientRendezvousFailureReason, - LegacyRendezvousFailureReason, - MSC4108FailureReason, -} from "matrix-js-sdk/src/rendezvous"; +import { ClientRendezvousFailureReason, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous"; import LoginWithQRFlow from "../../../../../../src/components/views/auth/LoginWithQRFlow"; import { LoginWithQRFailureReason, FailureReason } from "../../../../../../src/components/views/auth/LoginWithQR"; @@ -29,8 +25,7 @@ describe("", () => { phase: Phase; onClick?: () => Promise; failureReason?: FailureReason; - code?: string; - confirmationDigits?: string; + code?: Uint8Array; }) => ; beforeEach(() => {}); @@ -54,24 +49,14 @@ describe("", () => { }); it("renders QR code", async () => { - const { container } = render(getComponent({ phase: Phase.ShowingQR, code: "mock-code" })); + const { container } = render( + getComponent({ phase: Phase.ShowingQR, code: new TextEncoder().encode("mock-code") }), + ); // QR code is rendered async so we wait for it: await waitFor(() => screen.getAllByAltText("QR Code").length === 1); expect(container).toMatchSnapshot(); }); - it("renders code when connected", async () => { - const { container } = render(getComponent({ phase: Phase.LegacyConnected, confirmationDigits: "mock-digits" })); - expect(screen.getAllByText("mock-digits")).toHaveLength(1); - expect(screen.getAllByTestId("decline-login-button")).toHaveLength(1); - expect(screen.getAllByTestId("approve-login-button")).toHaveLength(1); - expect(container).toMatchSnapshot(); - fireEvent.click(screen.getByTestId("decline-login-button")); - expect(onClick).toHaveBeenCalledWith(Click.Decline, undefined); - fireEvent.click(screen.getByTestId("approve-login-button")); - expect(onClick).toHaveBeenCalledWith(Click.Approve, undefined); - }); - it("renders spinner while signing in", async () => { const { container } = render(getComponent({ phase: Phase.WaitingForDevice })); expect(screen.getAllByTestId("cancel-button")).toHaveLength(1); @@ -92,7 +77,6 @@ describe("", () => { describe("errors", () => { for (const failureReason of [ - ...Object.values(LegacyRendezvousFailureReason), ...Object.values(MSC4108FailureReason), ...Object.values(LoginWithQRFailureReason), ...Object.values(ClientRendezvousFailureReason), From 0454c5aacdd8754fc4b6da48169b29ef286d0d2a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Oct 2024 10:08:56 +0100 Subject: [PATCH 3/4] Remove stale snapshots --- .../LoginWithQRFlow-test.tsx.snap | 384 ------------------ 1 file changed, 384 deletions(-) diff --git a/test/unit-tests/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap b/test/unit-tests/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap index 9b6d83f4b6f..280ec75b042 100644 --- a/test/unit-tests/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap +++ b/test/unit-tests/components/views/settings/devices/__snapshots__/LoginWithQRFlow-test.tsx.snap @@ -252,48 +252,6 @@ exports[` errors renders expired 1`] = ` `; -exports[` errors renders expired 2`] = ` -
-
-
-
- - - -
-

- The sign in was not completed in time -

-

- Sign in expired. Please try again. -

-
-
-
-
-`; - exports[` errors renders homeserver_lacks_support 1`] = `
errors renders homeserver_lacks_support 1`] = `
`; -exports[` errors renders homeserver_lacks_support 2`] = ` -
-
-
-
- - - -
-
- Sessions - / - Link new device -
-
-
-
- - - - - -
-

- QR code not supported -

-

- Your account provider doesn't support signing into a new device with a QR code. -

-
-
-
-
-`; - exports[` errors renders insecure_channel_detected 1`] = `
errors renders unknown 1`] = `
`; -exports[` errors renders unknown 2`] = ` -
-
-
-
- - - -
-

- Something went wrong! -

-

- An unexpected error occurred. The request to connect your other device has been cancelled. -

-
-
-
-
-`; - -exports[` errors renders unsupported_algorithm 1`] = ` -
-
-
-
- - - -
-

- Something went wrong! -

-

- An unexpected error occurred. The request to connect your other device has been cancelled. -

-
-
-
-
-`; - exports[` errors renders unsupported_protocol 1`] = `
errors renders unsupported_protocol 1`] = `
`; -exports[` errors renders unsupported_protocol 2`] = ` -
-
-
-
- - - -
-

- Other device not compatible -

-

- This device does not support signing in to the other device with a QR code. -

-
-
-
-
-`; - exports[` errors renders user_cancelled 1`] = `
errors renders user_cancelled 1`] = `
`; -exports[` errors renders user_cancelled 2`] = ` -
-
-
-
- - - -
-

- Sign in request cancelled -

-

- The sign in was cancelled on the other device. -

-
-
-
-
-`; - exports[` errors renders user_declined 1`] = `
errors renders user_declined 1`] = `
`; -exports[` errors renders user_declined 2`] = ` -
-
-
-
- - - -
-

- Sign in declined -

-

- You or the account provider declined the sign in request. -

-
-
-
-
-`; - exports[` renders QR code 1`] = `
renders check code confirmation 1`] = `
`; -exports[` renders code when connected 1`] = ` -
-
-
-

- Check that the code below matches with your other device: -

-
- mock-digits -
-
-
-
-
-
- By approving access for this device, it will have full access to your account. -
-
-
-
-
- Approve -
-
- Cancel -
-
-
-
-`; - exports[` renders spinner while loading 1`] = `
Date: Wed, 23 Oct 2024 12:23:02 +0100 Subject: [PATCH 4/4] Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../settings/devices/LoginWithQR-test.tsx | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/test/unit-tests/components/views/settings/devices/LoginWithQR-test.tsx b/test/unit-tests/components/views/settings/devices/LoginWithQR-test.tsx index d38942fc6c0..218e43ac1f4 100644 --- a/test/unit-tests/components/views/settings/devices/LoginWithQR-test.tsx +++ b/test/unit-tests/components/views/settings/devices/LoginWithQR-test.tsx @@ -7,14 +7,18 @@ Please see LICENSE files in the repository root for full details. */ import { cleanup, render, waitFor } from "jest-matrix-react"; -import { MockedObject, mocked } from "jest-mock"; +import { mocked, MockedObject } from "jest-mock"; import React from "react"; -import { ClientRendezvousFailureReason, MSC4108SignInWithQR, MSC4108FailureReason } from "matrix-js-sdk/src/rendezvous"; -import { HTTPError } from "matrix-js-sdk/src/matrix"; +import { + ClientRendezvousFailureReason, + MSC4108FailureReason, + MSC4108SignInWithQR, + RendezvousError, +} from "matrix-js-sdk/src/rendezvous"; +import { HTTPError, MatrixClient } from "matrix-js-sdk/src/matrix"; import LoginWithQR from "../../../../../../src/components/views/auth/LoginWithQR"; import { Click, Mode, Phase } from "../../../../../../src/components/views/auth/LoginWithQR-types"; -import type { MatrixClient } from "matrix-js-sdk/src/matrix"; jest.mock("matrix-js-sdk/src/rendezvous"); jest.mock("matrix-js-sdk/src/rendezvous/transports"); @@ -141,6 +145,27 @@ describe("", () => { expect(global.window.open).toHaveBeenCalledWith("mock-verification-uri", "_blank"); }); + test("handles errors during protocol negotiation", async () => { + render(getComponent({ client })); + jest.spyOn(MSC4108SignInWithQR.prototype, "cancel").mockResolvedValue(); + const err = new RendezvousError("Unknown Failure", MSC4108FailureReason.UnsupportedProtocol); + // @ts-ignore work-around for lazy mocks + err.code = MSC4108FailureReason.UnsupportedProtocol; + jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockRejectedValue(err); + await waitFor(() => + expect(mockedFlow).toHaveBeenLastCalledWith( + expect.objectContaining({ + phase: Phase.ShowingQR, + }), + ), + ); + + await waitFor(() => { + const rendezvous = mocked(MSC4108SignInWithQR).mock.instances[0]; + expect(rendezvous.cancel).toHaveBeenCalledWith(MSC4108FailureReason.UnsupportedProtocol); + }); + }); + test("handles errors during reciprocation", async () => { render(getComponent({ client })); jest.spyOn(MSC4108SignInWithQR.prototype, "negotiateProtocols").mockResolvedValue({});