Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Add support for org.matrix.cross_signing_reset UIA stage flow #12892

Closed
wants to merge 13 commits into from
Closed
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
5 changes: 3 additions & 2 deletions src/components/structures/InteractiveAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -83,11 +84,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;
Expand Down
59 changes: 54 additions & 5 deletions src/components/views/auth/InteractiveAuthEntryComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -913,11 +916,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
Expand Down Expand Up @@ -975,6 +978,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;
Expand All @@ -991,8 +1038,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:
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("<EmailIdentityAuthEntry/>", () => {
Expand Down Expand Up @@ -63,3 +66,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({});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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>
`;
Loading