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<T> { // 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<ISSOAuthEntryProps, ISSOAuthEn } } -export class FallbackAuthEntry extends React.Component<IAuthEntryProps> { - private popupWindow: Window | null; - private fallbackButton = createRef<HTMLButtonElement>(); +export class FallbackAuthEntry<T = {}> extends React.Component<IAuthEntryProps & T> { + protected popupWindow: Window | null; + protected fallbackButton = createRef<HTMLButtonElement>(); - 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<IAuthEntryProps> { } } +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 ( + <div> + <Text>{_t("auth|uia|mas_cross_signing_reset_description")}</Text> + <Flex gap="var(--cpd-space-4x)"> + <Button + Icon={PopOutIcon} + onClick={this.onGoToAccountClick} + autoFocus + kind="primary" + className="mx_Dialog_nonDialogButton" + > + {_t("auth|uia|mas_cross_signing_reset_cta")} + </Button> + <Button onClick={this.onRetryClick} kind="secondary" className="mx_Dialog_nonDialogButton"> + {_t("action|retry")} + </Button> + </Flex> + </div> + ); + } +} + export interface IStageComponentProps extends IAuthEntryProps { stageParams?: Record<string, any>; inputs?: IInputs; @@ -983,8 +1030,10 @@ export interface IStageComponent extends React.ComponentClass<React.PropsWithRef focus?(): void; } -export default function getEntryComponentForLoginType(loginType: AuthType): IStageComponent { +export default function getEntryComponentForLoginType(loginType: AuthType | CustomAuthType): IStageComponent { switch (loginType) { + case CustomAuthType.MasCrossSigningReset: + return MasUnlockCrossSigningAuthEntry; case AuthType.Password: return PasswordAuthEntry; case AuthType.Recaptcha: diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 21addb3b98..e640bae337 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -369,6 +369,8 @@ "email_resend_prompt": "Did not receive it? <a>Resend it</a>", "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("<EmailIdentityAuthEntry/>", () => { @@ -55,3 +58,44 @@ describe("<EmailIdentityAuthEntry/>", () => { await waitFor(() => expect(screen.queryByRole("button", { name: "Resend" })).toBeInTheDocument()); }); }); + +describe("<MasUnlockCrossSigningAuthEntry/>", () => { + const renderAuth = (props = {}) => { + const matrixClient = createTestClient(); + + return render( + <MasUnlockCrossSigningAuthEntry + matrixClient={matrixClient} + loginType={AuthType.Email} + onPhaseChange={jest.fn()} + submitAuthDict={jest.fn()} + fail={jest.fn()} + clientSecret="my secret" + showContinue={true} + stageParams={{ url: "https://example.com" }} + {...props} + />, + ); + }; + + 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[`<EmailIdentityAuthEntry/> should render 1`] = ` </div> </div> `; + +exports[`<MasUnlockCrossSigningAuthEntry/> should render 1`] = ` +<div> + <div> + <p + class="_typography_yh5dq_162 _font-body-md-regular_yh5dq_59" + > + Reset your identity through your account provider and then come back and click “Retry”. + </p> + <div + class="mx_Flex" + style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x);" + > + <button + class="_button_zt6rp_17 mx_Dialog_nonDialogButton _has-icon_zt6rp_61" + data-kind="primary" + data-size="lg" + role="button" + tabindex="0" + > + <svg + aria-hidden="true" + fill="currentColor" + height="20" + viewBox="0 0 24 24" + width="20" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z" + /> + <path + d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2Z" + /> + </svg> + Go to your account + </button> + <button + class="_button_zt6rp_17 mx_Dialog_nonDialogButton" + data-kind="secondary" + data-size="lg" + role="button" + tabindex="0" + > + Retry + </button> + </div> + </div> +</div> +`;