Skip to content

Commit

Permalink
Merge pull request #1131 from tchapgouv/1123-sso-check-hs-configured
Browse files Browse the repository at this point in the history
feat(sso): add domain configured for sso check before login
  • Loading branch information
MarcWadai authored Oct 16, 2024
2 parents c6d1ae6 + 2dd6836 commit 17db437
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 29 deletions.
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);
});
});

0 comments on commit 17db437

Please sign in to comment.