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

Support registration tokens #7275

Merged
merged 24 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5b10be8
Support token authenticated registration
govynnus Dec 3, 2021
39c258a
Backwards compatibility with unstable auth type
govynnus Feb 18, 2022
8d3e1d9
Make LOGIN_TYPE public and readonly
govynnus Apr 10, 2022
1ddf830
Update registration-tokens to latest develop
govynnus Apr 10, 2022
f9a3fe8
Remove line related to skinning
govynnus Apr 10, 2022
47e3a7a
Change empty string to null
govynnus Apr 10, 2022
3a30db6
Use "public"s for new code style
govynnus May 12, 2022
e76180e
Merge branch 'develop' into registration-tokens
govynnus May 16, 2022
939a5ec
Change input to AccessibleButton
govynnus May 16, 2022
6fa9204
Add more detail regarding source of token
govynnus May 16, 2022
8e911d6
Merge remote-tracking branch 'upstream/develop' into registration-tokens
govynnus Sep 10, 2022
f489cd5
Fix lint error
govynnus Aug 12, 2022
6cb3260
Change null back to ""
govynnus Sep 10, 2022
3ebb998
Disable submit button when no token entered
govynnus Sep 10, 2022
a19b417
Add test for registration tokens
govynnus Sep 10, 2022
73e0f8a
Merge remote-tracking branch 'upstream/develop' into registration-tokens
govynnus Sep 24, 2022
43a3e4e
Merge remote-tracking branch 'upstream/develop' into registration-tokens
govynnus Oct 19, 2022
1881fa5
Merge remote-tracking branch 'upstream/master' into registration-tokens
govynnus Jan 17, 2023
04911e1
Merge remote-tracking branch 'upstream/develop' into registration-tokens
govynnus Jan 17, 2023
aea870d
Fix linting errors
govynnus Jan 18, 2023
3636932
Fix test for registration tokens
govynnus Jan 18, 2023
a632da7
Merge remote-tracking branch 'upstream/develop' into registration-tokens
govynnus Jan 18, 2023
3a944c0
Merge branch 'develop' into registration-tokens
andybalaam Jan 24, 2023
284ba03
Add missing type
andybalaam Jan 24, 2023
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
88 changes: 87 additions & 1 deletion src/components/views/auth/InteractiveAuthEntryComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,89 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
}
}

interface IRegistrationTokenAuthEntryState {
registrationToken: string;
}

export class RegistrationTokenAuthEntry extends React.Component<IAuthEntryProps, IRegistrationTokenAuthEntryState> {
public static readonly LOGIN_TYPE = AuthType.RegistrationToken;

public constructor(props: IAuthEntryProps) {
super(props);

this.state = {
registrationToken: "",
govynnus marked this conversation as resolved.
Show resolved Hide resolved
};
}

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<HTMLInputElement>): 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 = <Spinner />;
} else {
submitButtonOrSpinner = (
<AccessibleButton onClick={this.onSubmit} kind="primary" disabled={!this.state.registrationToken}>
{_t("Continue")}
</AccessibleButton>
);
}

let errorSection;
if (this.props.errorText) {
errorSection = (
<div className="error" role="alert">
{this.props.errorText}
</div>
);
}

return (
<div>
<p>{_t("Enter a registration token provided by the homeserver administrator.")}</p>
<form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_registrationTokenSection">
<Field
className={registrationTokenBoxClass}
type="text"
name="registrationTokenField"
label={_t("Registration token")}
autoFocus={true}
value={this.state.registrationToken}
onChange={this.onRegistrationTokenFieldChange}
/>
{errorSection}
<div className="mx_button_row">{submitButtonOrSpinner}</div>
</form>
</div>
);
}
}

interface ISSOAuthEntryProps extends IAuthEntryProps {
continueText?: string;
continueKind?: string;
Expand All @@ -713,7 +796,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
private ssoUrl: string;
private popupWindow: Window;

public constructor(props) {
public constructor(props: ISSOAuthEntryProps) {
super(props);

// We actually send the user through fallback auth so we don't have to
Expand Down Expand Up @@ -916,6 +999,9 @@ export default function getEntryComponentForLoginType(loginType: AuthType): ISta
return MsisdnAuthEntry;
case AuthType.Terms:
return TermsAuthEntry;
case AuthType.RegistrationToken:
case AuthType.UnstableRegistrationToken:
return RegistrationTokenAuthEntry;
case AuthType.Sso:
case AuthType.SsoUnstable:
return SSOAuthEntry;
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 @@ -3269,6 +3269,8 @@
"A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
"Please enter the code it contains:": "Please enter the code it contains:",
"Submit": "Submit",
"Enter a registration token provided by the homeserver administrator.": "Enter a registration token provided by the homeserver administrator.",
"Registration token": "Registration token",
"Something went wrong in confirming your identity. Cancel and try again.": "Something went wrong in confirming your identity. Cancel and try again.",
"Start authentication": "Start authentication",
"Sign in new device": "Sign in new device",
Expand Down
106 changes: 106 additions & 0 deletions test/components/views/auth/RegistrationToken-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022 Callum Brown

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import { act } from "react-dom/test-utils";
// eslint-disable-next-line deprecate/import
import { mount, ReactWrapper } from "enzyme";

import InteractiveAuthComponent from "../../../../src/components/structures/InteractiveAuth";
import { flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from "../../../test-utils";

describe("InteractiveAuthComponent", function () {
const mockClient = getMockClientWithEventEmitter({
generateClientSecret: jest.fn().mockReturnValue("t35tcl1Ent5ECr3T"),
});

const defaultProps = {
matrixClient: mockClient,
makeRequest: jest.fn().mockResolvedValue(undefined),
onAuthFinished: jest.fn(),
};
const getComponent = (props = {}) => mount(<InteractiveAuthComponent {...defaultProps} {...props} />);

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 },
);
});
});