From 6961c256035bed0b7640a6e5907652c806968478 Mon Sep 17 00:00:00 2001 From: Callum Brown Date: Tue, 24 Jan 2023 12:02:07 +0000 Subject: [PATCH] Support registration tokens (#7275) * Support token authenticated registration https://spec.matrix.org/v1.2/client-server-api/#token-authenticated-registration Signed-off-by: Callum Brown * Backwards compatibility with unstable auth type Some server installs are not updated to use the stable version of the registration token authentication type, so also handle the unstable version defined in MSC3231. Signed-off-by: Callum Brown * Make LOGIN_TYPE public and readonly Co-authored-by: Travis Ralston * Remove line related to skinning Signed-off-by: Callum Brown * Change empty string to null Signed-off-by: Callum Brown * Use "public"s for new code style Signed-off-by: Callum Brown * Change input to AccessibleButton Signed-off-by: Callum Brown * Add more detail regarding source of token Signed-off-by: Callum Brown * Fix lint error The text and button type will be the same every time for registration tokens, unlike (possibly) for SSO. Signed-off-by: Callum Brown * Change null back to "" Due to the following warning when attempting to test: > Warning: `value` prop on `input` should not be null. > Consider using an empty string to clear the component or > `undefined` for uncontrolled components. Signed-off-by: Callum Brown * Disable submit button when no token entered Signed-off-by: Callum Brown * Add test for registration tokens Adapted from test/components/views/dialogs/InteractiveAuthDialog-test.tsx Signed-off-by: Callum Brown * Fix linting errors Signed-off-by: Callum Brown * Fix test for registration tokens Signed-off-by: Callum Brown Signed-off-by: Callum Brown Co-authored-by: Travis Ralston Co-authored-by: Andy Balaam --- .../auth/InteractiveAuthEntryComponents.tsx | 88 ++++++++++++++- src/i18n/strings/en_EN.json | 2 + .../views/auth/RegistrationToken-test.tsx | 106 ++++++++++++++++++ 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 test/components/views/auth/RegistrationToken-test.tsx diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 78fcdf7c2ee..4a995e4d06b 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -692,6 +692,89 @@ export class MsisdnAuthEntry extends React.Component { + public static readonly LOGIN_TYPE = AuthType.RegistrationToken; + + public constructor(props: IAuthEntryProps) { + super(props); + + this.state = { + registrationToken: "", + }; + } + + public componentDidMount(): void { + this.props.onPhaseChange(DEFAULT_PHASE); + } + + private onSubmit = (e: FormEvent): void => { + e.preventDefault(); + if (this.props.busy) return; + + this.props.submitAuthDict({ + // Could be AuthType.RegistrationToken or AuthType.UnstableRegistrationToken + type: this.props.loginType, + token: this.state.registrationToken, + }); + }; + + private onRegistrationTokenFieldChange = (ev: ChangeEvent): void => { + // enable the submit button if the registration token is non-empty + this.setState({ + registrationToken: ev.target.value, + }); + }; + + public render(): JSX.Element { + const registrationTokenBoxClass = classNames({ + error: this.props.errorText, + }); + + let submitButtonOrSpinner; + if (this.props.busy) { + submitButtonOrSpinner = ; + } else { + submitButtonOrSpinner = ( + + {_t("Continue")} + + ); + } + + let errorSection; + if (this.props.errorText) { + errorSection = ( +
+ {this.props.errorText} +
+ ); + } + + return ( +
+

{_t("Enter a registration token provided by the homeserver administrator.")}

+
+ + {errorSection} +
{submitButtonOrSpinner}
+ +
+ ); + } +} + interface ISSOAuthEntryProps extends IAuthEntryProps { continueText?: string; continueKind?: string; @@ -713,7 +796,7 @@ export class SSOAuthEntry extends React.Component mount(); + + beforeEach(function () { + jest.clearAllMocks(); + }); + + afterAll(() => { + unmockClientPeg(); + }); + + const getSubmitButton = (wrapper: ReactWrapper) => wrapper.find('AccessibleButton[kind="primary"]').at(0); + const getRegistrationTokenInput = (wrapper: ReactWrapper) => + wrapper.find('input[name="registrationTokenField"]').at(0); + + it("Should successfully complete a registration token flow", async () => { + const onAuthFinished = jest.fn(); + const makeRequest = jest.fn().mockResolvedValue({ a: 1 }); + + const authData = { + session: "sess", + flows: [{ stages: ["m.login.registration_token"] }], + }; + + const wrapper = getComponent({ makeRequest, onAuthFinished, authData }); + + const registrationTokenNode = getRegistrationTokenInput(wrapper); + const submitNode = getSubmitButton(wrapper); + const formNode = wrapper.find("form").at(0); + + expect(registrationTokenNode).toBeTruthy(); + expect(submitNode).toBeTruthy(); + expect(formNode).toBeTruthy(); + + // submit should be disabled + expect(submitNode.props().disabled).toBe(true); + + // put something in the registration token box + act(() => { + registrationTokenNode.simulate("change", { target: { value: "s3kr3t" } }); + wrapper.setProps({}); + }); + + expect(getRegistrationTokenInput(wrapper).props().value).toEqual("s3kr3t"); + expect(getSubmitButton(wrapper).props().disabled).toBe(false); + + // hit enter; that should trigger a request + act(() => { + formNode.simulate("submit"); + }); + + // wait for auth request to resolve + await flushPromises(); + + expect(makeRequest).toHaveBeenCalledTimes(1); + expect(makeRequest).toBeCalledWith( + expect.objectContaining({ + session: "sess", + type: "m.login.registration_token", + token: "s3kr3t", + }), + ); + + expect(onAuthFinished).toBeCalledTimes(1); + expect(onAuthFinished).toBeCalledWith( + true, + { a: 1 }, + { clientSecret: "t35tcl1Ent5ECr3T", emailSid: undefined }, + ); + }); +});