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

Commit

Permalink
Support registration tokens (#7275)
Browse files Browse the repository at this point in the history
* Support token authenticated registration

https://spec.matrix.org/v1.2/client-server-api/#token-authenticated-registration

Signed-off-by: Callum Brown <[email protected]>

* 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 <[email protected]>

* Make LOGIN_TYPE public and readonly

Co-authored-by: Travis Ralston <[email protected]>

* Remove line related to skinning

Signed-off-by: Callum Brown <[email protected]>

* Change empty string to null

Signed-off-by: Callum Brown <[email protected]>

* Use "public"s for new code style

Signed-off-by: Callum Brown <[email protected]>

* Change input to AccessibleButton

Signed-off-by: Callum Brown <[email protected]>

* Add more detail regarding source of token

Signed-off-by: Callum Brown <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* Disable submit button when no token entered

Signed-off-by: Callum Brown <[email protected]>

* Add test for registration tokens

Adapted from test/components/views/dialogs/InteractiveAuthDialog-test.tsx

Signed-off-by: Callum Brown <[email protected]>

* Fix linting errors

Signed-off-by: Callum Brown <[email protected]>

* Fix test for registration tokens

Signed-off-by: Callum Brown <[email protected]>

Signed-off-by: Callum Brown <[email protected]>
Co-authored-by: Travis Ralston <[email protected]>
Co-authored-by: Andy Balaam <[email protected]>
  • Loading branch information
3 people committed Jan 24, 2023
1 parent 29c1932 commit 6961c25
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 1 deletion.
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: "",
};
}

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 @@ -3270,6 +3270,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 },
);
});
});

0 comments on commit 6961c25

Please sign in to comment.