Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sso): add domain configured for sso check before login #1131

Merged
merged 2 commits into from
Oct 16, 2024
Merged
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
4 changes: 4 additions & 0 deletions modules/tchap-translations/tchap_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -873,5 +873,9 @@
"auth|proconnect|or": {
"en": "or",
"fr": "ou"
},
"auth|proconnect|error_sso_inactive": {
"en": "ProConnect is disabled for your domain",
"fr": "ProConnect est désactivé pour votre domaine"
}
}
60 changes: 45 additions & 15 deletions src/tchap/components/views/sso/EmailVerificationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,26 @@ import { SSOAction } from "matrix-js-sdk/src/matrix";
import Login from "matrix-react-sdk/src/Login";
import TchapUtils from "../../../util/TchapUtils";
import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig";

import * as Email from "matrix-react-sdk/src/email";
import "../../../../../res/css/views/sso/TchapSSO.pcss";

export default function EmailVerificationPage() {

const [loading, setLoading] = useState<boolean>(false);
const [email, setEmail] = useState<string>("");
const [buttonDisabled, setButtonDisabled] = useState<boolean>(true);
const [errorText, setErrorText] = useState<string>("");

const submitButtonChild = loading ? <Spinner w={16} h={16} /> : _t("auth|proconnect|continue");

const emailFieldRef = useRef<Field>(null);

const checkEmailField = async (fieldString: string = email) : Promise<boolean> => {
const fieldOk = await emailFieldRef.current?.validate({ allowEmpty: false, focused: true });
return !!fieldOk && Email.looksValid(fieldString);
}

const displayError = (errorString: string): void => {
emailFieldRef.current?.focus();
emailFieldRef.current?.validate({ allowEmpty: false, focused: true });
setErrorText(errorString);
setLoading(false);
}
Expand All @@ -62,13 +66,18 @@ export default function EmailVerificationPage() {

}

const isSSOFlowActive = async (login: Login): Promise<boolean> => {
const flows = await login.getFlows();
return !!flows?.find((flow: Record<string, any>) => flow.type === "m.login.sso");
}

const onSubmit = async (event: React.FormEvent): Promise<void> => {
event.preventDefault();
setLoading(true);
const isFieldCorrect = await emailFieldRef.current?.validate({ allowEmpty: false });
const isFieldCorrect = await checkEmailField();

if (!isFieldCorrect) {
displayError(_td("auth|proconnect|error_email"));
displayError(_t("auth|proconnect|error_email"));
return;
}

Expand All @@ -81,29 +90,39 @@ export default function EmailVerificationPage() {
return;
}

const login = new Login(hs.base_url, hs.base_url, null, {});

const matrixClient= login.createTemporaryClient();

const validatedServerConfig = await setUpCurrentHs(hs);

if (!validatedServerConfig) {
displayError(_td("auth|proconnect|error_homeserver"));
displayError(_t("auth|proconnect|error_homeserver"));
return
}

const login = new Login(hs.base_url, hs.base_url, null, {});

const matrixClient= login.createTemporaryClient();
// check if oidc is activated on HS
const canSSO = await isSSOFlowActive(login);
if (!canSSO) {
displayError(_t("auth|proconnect|error_sso_inactive"));
return
}

// start SSO flow since we got the homeserver
PlatformPeg.get()?.startSingleSignOn(matrixClient, "sso", "/home", "", SSOAction.LOGIN);

setLoading(false);

} catch(err) {
displayError(_td("auth|proconnect|error"));
displayError(_t("auth|proconnect|error"));
}
}

const onInputChanged = (event: React.FormEvent<HTMLInputElement>) => {
setEmail(event.currentTarget.value);
const onInputChanged = async (event: React.FormEvent<HTMLInputElement>) => {
const emailString = event.currentTarget.value
setEmail(emailString);
const isEmailValid = await checkEmailField(emailString);
setButtonDisabled(!isEmailValid);
}

const onLoginByPasswordClick = () => {
Expand Down Expand Up @@ -132,9 +151,20 @@ export default function EmailVerificationPage() {
/>
</div>
{errorText && <ErrorMessage message={errorText} />}
<button type="submit" data-testid="proconnect-submit" className="tc_ButtonParent tc_ButtonProconnect tc_Button_iconPC">
{submitButtonChild}
</button>
<AccessibleButton
type="submit"
data-testid="proconnect-submit"
title={_t("auth|proconnect|continue")}
className="tc_ButtonParent tc_ButtonProconnect tc_Button_iconPC"
element="button"
kind="link"
disabled={buttonDisabled}
onClick={(e: ButtonEvent) => {
onSubmit(e);
}}
>
{submitButtonChild}
</AccessibleButton>
<div className="mx_AuthBody_button-container tc_bottomButton">
<AccessibleButton
className="mx_AuthBody_sign-in-instead-button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
import EmailVerificationPage from "~tchap-web/src/tchap/components/views/sso/EmailVerificationPage";
import TchapUtils from "~tchap-web/src/tchap/util/TchapUtils";
import { ValidatedServerConfig } from "~matrix-react-sdk/src/utils/ValidatedServerConfig";
import { mockPlatformPeg, stubClient } from "~matrix-react-sdk/test/test-utils";
import { flushPromises, mockPlatformPeg, stubClient } from "~matrix-react-sdk/test/test-utils";
import BasePlatform from "~matrix-react-sdk/src/BasePlatform";
import Login from "~matrix-react-sdk/src/Login";

Expand All @@ -22,12 +22,7 @@ describe("<EmailVerificationPage />", () => {
const PlatformPegMocked: MockedObject<BasePlatform> = mockPlatformPeg();
const mockedClient: MatrixClient = stubClient();
const mockedTchapUtils = mocked(TchapUtils);

const mockLoginObject = (hs: string = defaultHsUrl) => {
const mockLoginObject = mocked(new Login(hs, hs, null, {}));
mockLoginObject.createTemporaryClient.mockImplementation(() => mockedClient);
return mockLoginObject;
};
const mockedLogin = Login as jest.Mock;

const mockedFetchHomeserverFromEmail = (hs: string = defaultHsUrl) => {
mockedTchapUtils.fetchHomeserverForEmail.mockImplementation(() =>
Expand Down Expand Up @@ -68,7 +63,11 @@ describe("<EmailVerificationPage />", () => {
const renderEmailVerificationPage = () => render(<EmailVerificationPage />);

beforeEach(() => {
mockLoginObject(defaultHsUrl);
mockedLogin.mockImplementation(() => ({
hsUrl: defaultHsUrl,
createTemporaryClient: jest.fn().mockReturnValue(mockedClient),
getFlows: jest.fn().mockResolvedValue([{ type: "m.login.sso" }]),
}));
});

afterEach(() => {
Expand All @@ -77,7 +76,7 @@ describe("<EmailVerificationPage />", () => {
});

it("returns error when empty email", async () => {
const { container } = renderEmailVerificationPage();
renderEmailVerificationPage();

// Put text in email field
const emailField = screen.getByRole("textbox");
Expand All @@ -86,16 +85,17 @@ describe("<EmailVerificationPage />", () => {

// click on proconnect button
const proconnectButton = screen.getByTestId("proconnect-submit");

await act(async () => {
await fireEvent.click(proconnectButton);
});

// Error classes should not appear
expect(container.getElementsByClassName("mx_ErrorMessage").length).toBe(1);
// Submit button should be disabled
expect(proconnectButton).toHaveAttribute("disabled");
});

it("returns inccorrect email", async () => {
const { container } = renderEmailVerificationPage();
renderEmailVerificationPage();

// Put text in email field
const emailField = screen.getByRole("textbox");
Expand All @@ -108,8 +108,8 @@ describe("<EmailVerificationPage />", () => {
await fireEvent.click(proconnectButton);
});

// Error classes should not appear
expect(container.getElementsByClassName("mx_ErrorMessage").length).toBe(1);
// Submit button should be disabled
expect(proconnectButton).toHaveAttribute("disabled");
});

it("should throw error when homeserver catch an error", async () => {
Expand All @@ -124,6 +124,7 @@ describe("<EmailVerificationPage />", () => {
fireEvent.focus(emailField);
fireEvent.change(emailField, { target: { value: userEmail } });

await flushPromises();
// click on proconnect button
const proconnectButton = screen.getByTestId("proconnect-submit");
await act(async () => {
Expand All @@ -146,6 +147,8 @@ describe("<EmailVerificationPage />", () => {
fireEvent.focus(emailField);
fireEvent.change(emailField, { target: { value: userEmail } });

await flushPromises();

// click on proconnect button
const proconnectButton = screen.getByTestId("proconnect-submit");
await act(async () => {
Expand All @@ -169,6 +172,8 @@ describe("<EmailVerificationPage />", () => {
fireEvent.focus(emailField);
fireEvent.change(emailField, { target: { value: userEmail } });

await flushPromises();

// click on proconnect button
const proconnectButton = screen.getByTestId("proconnect-submit");
await act(async () => {
Expand All @@ -195,6 +200,8 @@ describe("<EmailVerificationPage />", () => {
fireEvent.focus(emailField);
fireEvent.change(emailField, { target: { value: userEmail } });

await flushPromises();

// click on proconnect button
const proconnectButton = screen.getByTestId("proconnect-submit");
await act(async () => {
Expand All @@ -207,4 +214,33 @@ describe("<EmailVerificationPage />", () => {
});
expect(PlatformPegMocked.startSingleSignOn).toHaveBeenCalledTimes(1);
});

it("should display error when sso is not configured in homeserer", async () => {
const { container } = renderEmailVerificationPage();

// Mock the implementation without error, what we want is to be sure they are called with the correct parameters
mockedFetchHomeserverFromEmail(secondHsUrl);
mockedValidatedServerConfig(false, secondHsUrl);
mockedPlatformPegStartSSO(false);
// get flow without sso configured on homeserver
mockedLogin.mockImplementation(() => ({
hsUrl: secondHsUrl,
createTemporaryClient: jest.fn().mockReturnValue(mockedClient),
getFlows: jest.fn().mockResolvedValue([{ type: "m.login.password" }]),
}));
// Put text in email field
const emailField = screen.getByRole("textbox");
fireEvent.focus(emailField);
fireEvent.change(emailField, { target: { value: userEmail } });

await flushPromises();

// click on proconnect button
const proconnectButton = screen.getByTestId("proconnect-submit");
await act(async () => {
await fireEvent.click(proconnectButton);
});

expect(container.getElementsByClassName("mx_ErrorMessage").length).toBe(1);
});
});
Loading