From be59791db1605e136814d37d29dc9f6b68dcb440 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 13 Sep 2024 12:49:19 +0100 Subject: [PATCH] Add support for `org.matrix.cross_signing_reset` UIA stage flow (#34) * Soften UIA fallback postMessage check to work cross-origin Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Do the same for the SSO UIA flow Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add support for `org.matrix.cross_signing_reset` UIA stage flow Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Check against MessageEvent::source instead Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove protected method Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/InteractiveAuth.tsx | 5 +- .../auth/InteractiveAuthEntryComponents.tsx | 59 +++++++++++++++++-- src/i18n/strings/en_EN.json | 2 + .../InteractiveAuthEntryComponents-test.tsx | 48 ++++++++++++++- ...teractiveAuthEntryComponents-test.tsx.snap | 50 ++++++++++++++++ 5 files changed, 155 insertions(+), 9 deletions(-) diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 86cf6af665..91e52a1905 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -20,6 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import getEntryComponentForLoginType, { ContinueKind, + CustomAuthType, IStageComponent, } from "../views/auth/InteractiveAuthEntryComponents"; import Spinner from "../views/elements/Spinner"; @@ -75,11 +76,11 @@ export interface InteractiveAuthProps { // Called when the stage changes, or the stage's phase changes. First // argument is the stage, second is the phase. Some stages do not have // phases and will be counted as 0 (numeric). - onStagePhaseChange?(stage: AuthType | null, phase: number): void; + onStagePhaseChange?(stage: AuthType | CustomAuthType | null, phase: number): void; } interface IState { - authStage?: AuthType; + authStage?: CustomAuthType | AuthType; stageState?: IStageStatus; busy: boolean; errorText?: string; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index a0946564aa..7a15ee2095 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -11,6 +11,8 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { AuthType, AuthDict, IInputs, IStageStatus } from "matrix-js-sdk/src/interactive-auth"; import { logger } from "matrix-js-sdk/src/logger"; import React, { ChangeEvent, createRef, FormEvent, Fragment } from "react"; +import { Button, Text } from "@vector-im/compound-web"; +import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; import EmailPromptIcon from "../../../../res/img/element-icons/email-prompt.svg"; import { _t } from "../../../languageHandler"; @@ -21,6 +23,7 @@ import AccessibleButton, { AccessibleButtonKind, ButtonEvent } from "../elements import Field from "../elements/Field"; import Spinner from "../elements/Spinner"; import CaptchaForm from "./CaptchaForm"; +import { Flex } from "../../utils/Flex"; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -905,11 +908,11 @@ export class SSOAuthEntry extends React.Component { - private popupWindow: Window | null; - private fallbackButton = createRef(); +export class FallbackAuthEntry extends React.Component { + protected popupWindow: Window | null; + protected fallbackButton = createRef(); - public constructor(props: IAuthEntryProps) { + public constructor(props: IAuthEntryProps & T) { super(props); // we have to make the user click a button, as browsers will block @@ -967,6 +970,50 @@ export class FallbackAuthEntry extends React.Component { } } +export enum CustomAuthType { + // Workaround for MAS requiring non-UIA authentication for resetting cross-signing. + MasCrossSigningReset = "org.matrix.cross_signing_reset", +} + +export class MasUnlockCrossSigningAuthEntry extends FallbackAuthEntry<{ + stageParams?: { + url?: string; + }; +}> { + public static LOGIN_TYPE = CustomAuthType.MasCrossSigningReset; + + private onGoToAccountClick = (): void => { + if (!this.props.stageParams?.url) return; + this.popupWindow = window.open(this.props.stageParams.url, "_blank"); + }; + + private onRetryClick = (): void => { + this.props.submitAuthDict({}); + }; + + public render(): React.ReactNode { + return ( +
+ {_t("auth|uia|mas_cross_signing_reset_description")} + + + + +
+ ); + } +} + export interface IStageComponentProps extends IAuthEntryProps { stageParams?: Record; inputs?: IInputs; @@ -983,8 +1030,10 @@ export interface IStageComponent extends React.ComponentClassResend it", "email_resent": "Resent!", "fallback_button": "Start authentication", + "mas_cross_signing_reset_cta": "Go to your account", + "mas_cross_signing_reset_description": "Reset your identity through your account provider and then come back and click “Retry”.", "msisdn": "A text message has been sent to %(msisdn)s", "msisdn_token_incorrect": "Token incorrect", "msisdn_token_prompt": "Please enter the code it contains:", diff --git a/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx b/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx index 62c02b0d58..1cbf799af7 100644 --- a/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx +++ b/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx @@ -7,11 +7,14 @@ */ import React from "react"; -import { render, screen, waitFor, act } from "@testing-library/react"; +import { render, screen, waitFor, act, fireEvent } from "@testing-library/react"; import { AuthType } from "matrix-js-sdk/src/interactive-auth"; import userEvent from "@testing-library/user-event"; -import { EmailIdentityAuthEntry } from "../../../../src/components/views/auth/InteractiveAuthEntryComponents"; +import { + EmailIdentityAuthEntry, + MasUnlockCrossSigningAuthEntry, +} from "../../../../src/components/views/auth/InteractiveAuthEntryComponents"; import { createTestClient } from "../../../test-utils"; describe("", () => { @@ -55,3 +58,44 @@ describe("", () => { await waitFor(() => expect(screen.queryByRole("button", { name: "Resend" })).toBeInTheDocument()); }); }); + +describe("", () => { + const renderAuth = (props = {}) => { + const matrixClient = createTestClient(); + + return render( + , + ); + }; + + test("should render", () => { + const { container } = renderAuth(); + expect(container).toMatchSnapshot(); + }); + + test("should open idp in new tab on click", async () => { + const spy = jest.spyOn(global.window, "open"); + renderAuth(); + + fireEvent.click(screen.getByRole("button", { name: "Go to your account" })); + expect(spy).toHaveBeenCalledWith("https://example.com", "_blank"); + }); + + test("should retry uia request on click", async () => { + const submitAuthDict = jest.fn(); + renderAuth({ submitAuthDict }); + + fireEvent.click(screen.getByRole("button", { name: "Retry" })); + expect(submitAuthDict).toHaveBeenCalledWith({}); + }); +}); diff --git a/test/components/views/auth/__snapshots__/InteractiveAuthEntryComponents-test.tsx.snap b/test/components/views/auth/__snapshots__/InteractiveAuthEntryComponents-test.tsx.snap index 65f86a35d2..16e5b3abc2 100644 --- a/test/components/views/auth/__snapshots__/InteractiveAuthEntryComponents-test.tsx.snap +++ b/test/components/views/auth/__snapshots__/InteractiveAuthEntryComponents-test.tsx.snap @@ -32,3 +32,53 @@ exports[` should render 1`] = ` `; + +exports[` should render 1`] = ` +
+
+

+ Reset your identity through your account provider and then come back and click “Retry”. +

+
+ + +
+
+
+`;