From 34f9404959f5c47df56820628860fb31176ba51c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Aug 2024 13:59:48 +0100 Subject: [PATCH 1/7] Soften UIA fallback postMessage check to work cross-origin Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/InteractiveAuthEntryComponents.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 7bed60d6037..f9572399017 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -950,7 +950,9 @@ export class FallbackAuthEntry extends React.Component { }; private onReceiveMessage = (event: MessageEvent): void => { - if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) { + // We don't check the origin here as we don't trust any incoming data and just use it as a ping to retry the request, + // and the HS may delegate the fallback to another origin, due to CORS we cannot inspect the origin of the popupWindow. + if (event.data === "authDone") { this.props.submitAuthDict({}); } }; From 0b64177b7a03cc8180a739102003f8bd94b5b7f2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 14 Aug 2024 10:31:00 +0100 Subject: [PATCH 2/7] Do the same for the SSO UIA flow Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/InteractiveAuthEntryComponents.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index f9572399017..de609f0f987 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -833,7 +833,9 @@ export class SSOAuthEntry extends React.Component { - if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) { + // We don't check the origin here as we don't trust any incoming data and just use it as a ping to retry the request, + // and the HS may delegate the fallback to another origin, due to CORS we cannot inspect the origin of the popupWindow. + if (event.data === "authDone") { if (this.popupWindow) { this.popupWindow.close(); this.popupWindow = null; From 5b5b305364e475400cbb7302305f0a5180461c92 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Aug 2024 11:03:13 +0100 Subject: [PATCH 3/7] Add support for `org.matrix.cross_signing_reset` UIA stage flow Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/InteractiveAuth.tsx | 5 +- .../auth/InteractiveAuthEntryComponents.tsx | 61 +++++++++++++++++-- src/i18n/strings/en_EN.json | 4 +- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 1ca2b6e5ce3..d2e240d6f29 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -28,6 +28,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import getEntryComponentForLoginType, { ContinueKind, + CustomAuthType, IStageComponent, } from "../views/auth/InteractiveAuthEntryComponents"; import Spinner from "../views/elements/Spinner"; @@ -83,11 +84,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 de609f0f987..7313f6afebf 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -19,6 +19,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"; @@ -29,6 +31,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 @@ -915,11 +918,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 @@ -951,7 +954,7 @@ export class FallbackAuthEntry extends React.Component { this.popupWindow = window.open(url, "_blank"); }; - private onReceiveMessage = (event: MessageEvent): void => { + protected onReceiveMessage = (event: MessageEvent): void => { // We don't check the origin here as we don't trust any incoming data and just use it as a ping to retry the request, // and the HS may delegate the fallback to another origin, due to CORS we cannot inspect the origin of the popupWindow. if (event.data === "authDone") { @@ -979,6 +982,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; @@ -995,8 +1042,10 @@ export interface IStageComponent extends React.ComponentClass Date: Thu, 15 Aug 2024 11:04:57 +0100 Subject: [PATCH 4/7] Check against MessageEvent::source instead Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/auth/InteractiveAuthEntryComponents.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index de609f0f987..d54b52c1a0f 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -833,9 +833,7 @@ export class SSOAuthEntry extends React.Component { - // We don't check the origin here as we don't trust any incoming data and just use it as a ping to retry the request, - // and the HS may delegate the fallback to another origin, due to CORS we cannot inspect the origin of the popupWindow. - if (event.data === "authDone") { + if (event.data === "authDone" && event.source === this.popupWindow) { if (this.popupWindow) { this.popupWindow.close(); this.popupWindow = null; @@ -952,9 +950,7 @@ export class FallbackAuthEntry extends React.Component { }; private onReceiveMessage = (event: MessageEvent): void => { - // We don't check the origin here as we don't trust any incoming data and just use it as a ping to retry the request, - // and the HS may delegate the fallback to another origin, due to CORS we cannot inspect the origin of the popupWindow. - if (event.data === "authDone") { + if (event.data === "authDone" && event.source === this.popupWindow) { this.props.submitAuthDict({}); } }; From cfb1a38d65833eee21d7156fb8eba536829840c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Aug 2024 11:43:34 +0100 Subject: [PATCH 5/7] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a9b595c6012..fa2c5fc623e 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? Resend 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:", @@ -383,9 +385,7 @@ "sso_preauth_body": "To continue, use Single Sign On to prove your identity.", "sso_title": "Use Single Sign On to continue", "terms": "Please review and accept the policies of this homeserver:", - "terms_invalid": "Please review and accept all of the homeserver's policies", - "mas_cross_signing_reset_description": "Reset your identity through your account provider and then come back and click “Retry”.", - "mas_cross_signing_reset_cta": "Go to your account" + "terms_invalid": "Please review and accept all of the homeserver's policies" }, "unsupported_auth": "This homeserver doesn't offer any login flows that are supported by this client.", "unsupported_auth_email": "This homeserver does not support login using email address.", From fc8db6b37baa4245269524c36b97c2463862cf9a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Aug 2024 09:44:12 +0100 Subject: [PATCH 6/7] Add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../InteractiveAuthEntryComponents-test.tsx | 48 +++++++++++++++++- ...teractiveAuthEntryComponents-test.tsx.snap | 50 +++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx b/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx index e6e3e1383e3..92b8c8610d3 100644 --- a/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx +++ b/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx @@ -15,11 +15,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("", () => { @@ -63,3 +66,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 65f86a35d2a..16e5b3abc25 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”. +

+
+ + +
+
+
+`; From 8cf60e034c3751f519876746bd039382603e49a2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Aug 2024 09:52:42 +0100 Subject: [PATCH 7/7] Remove protected method Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/InteractiveAuthEntryComponents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index b31a126d374..12c2f8f975e 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -952,7 +952,7 @@ export class FallbackAuthEntry extends React.Component { + private onReceiveMessage = (event: MessageEvent): void => { if (event.data === "authDone" && event.source === this.popupWindow) { this.props.submitAuthDict({}); }