0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -05:00

feat(experience): block email sign-in and register (#4886)

* feat(experience): block email sign-in and register

block email sign-in and register if SSO is enabled for that domain address

* test(experience): add ut

add ut
This commit is contained in:
simeng-li 2023-11-17 15:41:35 +08:00 committed by GitHub
parent 2c1cf66c6b
commit e68637f12d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 388 additions and 108 deletions

View file

@ -49,82 +49,84 @@ const App = () => {
<BrowserRouter> <BrowserRouter>
<PageContextProvider> <PageContextProvider>
<SettingsProvider> <SettingsProvider>
<AppBoundary> <SingleSignOnContextProvider>
<AppInsightsBoundary cloudRole="ui"> <AppBoundary>
<Routes> <AppInsightsBoundary cloudRole="ui">
<Route path="sign-in/consent" element={<Consent />} /> <Routes>
<Route element={<AppLayout />}> <Route path="sign-in/consent" element={<Consent />} />
<Route <Route element={<AppLayout />}>
path="unknown-session" <Route
element={<ErrorPage message="error.invalid_session" />} path="unknown-session"
/> element={<ErrorPage message="error.invalid_session" />}
<Route path="springboard" element={<Springboard />} /> />
<Route path="springboard" element={<Springboard />} />
<Route element={<LoadingLayerProvider />}> <Route element={<LoadingLayerProvider />}>
{/* Sign-in */} {/* Sign-in */}
<Route path="sign-in"> <Route path="sign-in">
<Route index element={<SignIn />} /> <Route index element={<SignIn />} />
<Route path="password" element={<SignInPassword />} /> <Route path="password" element={<SignInPassword />} />
<Route path="social/:connectorId" element={<SocialSignIn />} /> <Route path="social/:connectorId" element={<SocialSignIn />} />
</Route>
{/* Register */}
<Route path="register">
<Route index element={<Register />} />
<Route path="password" element={<RegisterPassword />} />
</Route>
{/* Forgot password */}
<Route path="forgot-password">
<Route index element={<ForgotPassword />} />
<Route path="reset" element={<ResetPassword />} />
</Route>
{/* Passwordless verification code */}
<Route path=":flow/verification-code" element={<VerificationCode />} />
{/* Mfa binding */}
<Route path={UserMfaFlow.MfaBinding}>
<Route index element={<MfaBinding />} />
<Route path={MfaFactor.TOTP} element={<TotpBinding />} />
<Route path={MfaFactor.WebAuthn} element={<WebAuthnBinding />} />
<Route path={MfaFactor.BackupCode} element={<BackupCodeBinding />} />
</Route>
{/* Mfa verification */}
<Route path={UserMfaFlow.MfaVerification}>
<Route index element={<MfaVerification />} />
<Route path={MfaFactor.TOTP} element={<TotpVerification />} />
<Route path={MfaFactor.WebAuthn} element={<WebAuthnVerification />} />
<Route path={MfaFactor.BackupCode} element={<BackupCodeVerification />} />
</Route>
{/* Continue set up missing profile */}
<Route path="continue">
<Route path=":method" element={<Continue />} />
</Route>
{/* Social sign-in pages */}
<Route path="social">
<Route path="link/:connectorId" element={<SocialLinkAccount />} />
<Route path="landing/:connectorId" element={<SocialLanding />} />
</Route>
<Route path="callback/:connectorId" element={<Callback />} />
</Route> </Route>
{/* Register */} {/* Single sign on */}
<Route path="register"> {isDevelopmentFeaturesEnabled && (
<Route index element={<Register />} /> <Route path={singleSignOnPath}>
<Route path="password" element={<RegisterPassword />} /> <Route path="email" element={<SingleSignOnEmail />} />
</Route> <Route path="connectors" element={<SingleSignOnConnectors />} />
</Route>
)}
{/* Forgot password */} <Route path="*" element={<ErrorPage />} />
<Route path="forgot-password">
<Route index element={<ForgotPassword />} />
<Route path="reset" element={<ResetPassword />} />
</Route>
{/* Passwordless verification code */}
<Route path=":flow/verification-code" element={<VerificationCode />} />
{/* Mfa binding */}
<Route path={UserMfaFlow.MfaBinding}>
<Route index element={<MfaBinding />} />
<Route path={MfaFactor.TOTP} element={<TotpBinding />} />
<Route path={MfaFactor.WebAuthn} element={<WebAuthnBinding />} />
<Route path={MfaFactor.BackupCode} element={<BackupCodeBinding />} />
</Route>
{/* Mfa verification */}
<Route path={UserMfaFlow.MfaVerification}>
<Route index element={<MfaVerification />} />
<Route path={MfaFactor.TOTP} element={<TotpVerification />} />
<Route path={MfaFactor.WebAuthn} element={<WebAuthnVerification />} />
<Route path={MfaFactor.BackupCode} element={<BackupCodeVerification />} />
</Route>
{/* Continue set up missing profile */}
<Route path="continue">
<Route path=":method" element={<Continue />} />
</Route>
{/* Social sign-in pages */}
<Route path="social">
<Route path="link/:connectorId" element={<SocialLinkAccount />} />
<Route path="landing/:connectorId" element={<SocialLanding />} />
</Route>
<Route path="callback/:connectorId" element={<Callback />} />
</Route> </Route>
</Routes>
{/* Single sign on */} </AppInsightsBoundary>
{isDevelopmentFeaturesEnabled && ( </AppBoundary>
<Route path={singleSignOnPath} element={<SingleSignOnContextProvider />}> </SingleSignOnContextProvider>
<Route path="email" element={<SingleSignOnEmail />} />
<Route path="connectors" element={<SingleSignOnConnectors />} />
</Route>
)}
<Route path="*" element={<ErrorPage />} />
</Route>
</Routes>
</AppInsightsBoundary>
</AppBoundary>
</SettingsProvider> </SettingsProvider>
</PageContextProvider> </PageContextProvider>
</BrowserRouter> </BrowserRouter>

View file

@ -1,13 +1,16 @@
import { type SsoConnectorMetadata } from '@logto/schemas'; import { type SsoConnectorMetadata } from '@logto/schemas';
import { useEffect, useMemo, useState } from 'react'; import { type ReactNode, useEffect, useMemo, useState } from 'react';
import { Outlet } from 'react-router-dom';
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages'; import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
import { useSieMethods } from '@/hooks/use-sie'; import { useSieMethods } from '@/hooks/use-sie';
import SingleSignOnContext, { type SingleSignOnContextType } from './SingleSignOnContext'; import SingleSignOnContext, { type SingleSignOnContextType } from './SingleSignOnContext';
const SingleSignOnContextProvider = () => { type Props = {
children: ReactNode;
};
const SingleSignOnContextProvider = ({ children }: Props) => {
const { ssoConnectors } = useSieMethods(); const { ssoConnectors } = useSieMethods();
const { get, set, remove } = useSessionStorage(); const { get, set, remove } = useSessionStorage();
const [email, setEmail] = useState<string | undefined>(get(StorageKeys.SsoEmail)); const [email, setEmail] = useState<string | undefined>(get(StorageKeys.SsoEmail));
@ -51,7 +54,7 @@ const SingleSignOnContextProvider = () => {
return ( return (
<SingleSignOnContext.Provider value={singleSignOnContext}> <SingleSignOnContext.Provider value={singleSignOnContext}>
<Outlet /> {children}
</SingleSignOnContext.Provider> </SingleSignOnContext.Provider>
); );
}; };

View file

@ -5,10 +5,11 @@ import { useNavigate } from 'react-router-dom';
import SingleSignOnContext from '@/Providers/SingleSignOnContextProvider/SingleSignOnContext'; import SingleSignOnContext from '@/Providers/SingleSignOnContextProvider/SingleSignOnContext';
import { getSingleSignOnConnectors } from '@/apis/single-sign-on'; import { getSingleSignOnConnectors } from '@/apis/single-sign-on';
import { singleSignOnPath } from '@/constants/env';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler'; import useErrorHandler from '@/hooks/use-error-handler';
const useOnSubmit = () => { const useCheckSingleSignOn = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const request = useApi(getSingleSignOnConnectors); const request = useApi(getSingleSignOnConnectors);
@ -29,6 +30,11 @@ const useOnSubmit = () => {
setSsoConnectors([]); setSsoConnectors([]);
}, [setEmail, setSsoConnectors]); }, [setEmail, setSsoConnectors]);
/**
* Check if the email is registered with any SSO connectors
* @param {string} email
* @returns {Promise<boolean>} - true if the email is registered with any SSO connectors
*/
const onSubmit = useCallback( const onSubmit = useCallback(
async (email: string) => { async (email: string) => {
clearContext(); clearContext();
@ -57,7 +63,8 @@ const useOnSubmit = () => {
setSsoConnectors(connectors); setSsoConnectors(connectors);
setEmail(email); setEmail(email);
navigate('../connectors'); navigate(`/${singleSignOnPath}/connectors`);
return true;
}, },
[ [
availableSsoConnectorsMap, availableSsoConnectorsMap,
@ -78,4 +85,4 @@ const useOnSubmit = () => {
}; };
}; };
export default useOnSubmit; export default useCheckSingleSignOn;

View file

@ -1,18 +1,22 @@
import { SignInIdentifier } from '@logto/schemas'; import { SignInIdentifier, type SsoConnectorMetadata } from '@logto/schemas';
import { assert } from '@silverhand/essentials'; import { assert } from '@silverhand/essentials';
import { fireEvent, act, waitFor } from '@testing-library/react'; import { fireEvent, act, waitFor } from '@testing-library/react';
import ConfirmModalProvider from '@/Providers/ConfirmModalProvider'; import ConfirmModalProvider from '@/Providers/ConfirmModalProvider';
import SingleSignOnContextProvider from '@/Providers/SingleSignOnContextProvider';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { mockSignInExperienceSettings, mockSsoConnectors } from '@/__mocks__/logto';
import { registerWithUsernamePassword } from '@/apis/interaction'; import { registerWithUsernamePassword } from '@/apis/interaction';
import { sendVerificationCodeApi } from '@/apis/utils'; import { sendVerificationCodeApi } from '@/apis/utils';
import { singleSignOnPath } from '@/constants/env';
import { UserFlow } from '@/types'; import { UserFlow } from '@/types';
import { getDefaultCountryCallingCode } from '@/utils/country-code'; import { getDefaultCountryCallingCode } from '@/utils/country-code';
import IdentifierRegisterForm from '.'; import IdentifierRegisterForm from '.';
const mockedNavigate = jest.fn(); const mockedNavigate = jest.fn();
const getSingleSignOnConnectorsMock = jest.fn();
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
@ -33,11 +37,25 @@ jest.mock('@/apis/interaction', () => ({
registerWithUsernamePassword: jest.fn(async () => ({})), registerWithUsernamePassword: jest.fn(async () => ({})),
})); }));
const renderForm = (signUpMethods: SignInIdentifier[] = [SignInIdentifier.Username]) => { jest.mock('@/apis/single-sign-on', () => ({
getSingleSignOnConnectors: () => getSingleSignOnConnectorsMock(),
}));
const renderForm = (
signUpMethods: SignInIdentifier[] = [SignInIdentifier.Username],
ssoConnectors: SsoConnectorMetadata[] = []
) => {
return renderWithPageContext( return renderWithPageContext(
<SettingsProvider> <SettingsProvider
settings={{
...mockSignInExperienceSettings,
ssoConnectors,
}}
>
<ConfirmModalProvider> <ConfirmModalProvider>
<IdentifierRegisterForm signUpMethods={signUpMethods} /> <SingleSignOnContextProvider>
<IdentifierRegisterForm signUpMethods={signUpMethods} />
</SingleSignOnContextProvider>
</ConfirmModalProvider> </ConfirmModalProvider>
</SettingsProvider> </SettingsProvider>
); );
@ -282,4 +300,85 @@ describe('<IdentifierRegisterForm />', () => {
}); });
} }
); );
describe('single sign on register form', () => {
const email = 'foo@email.com';
it('should not call check single sign-on connector when no single sign-on connector is enabled', async () => {
const { getByText, container } = renderForm([SignInIdentifier.Email]);
const submitButton = getByText('action.create_account');
const emailInput = container.querySelector('input[name="identifier"]');
const termsButton = getByText('description.agree_with_terms');
assert(emailInput, new Error('username input not found'));
act(() => {
fireEvent.change(emailInput, { target: { value: email } });
fireEvent.click(termsButton);
});
act(() => {
fireEvent.submit(submitButton);
});
await waitFor(() => {
expect(getSingleSignOnConnectorsMock).not.toBeCalled();
expect(sendVerificationCodeApi).toBeCalledWith(UserFlow.Register, {
email,
});
});
});
it('should call check single sign-on connector when the identifier is email, but process to password sign-in if no sso connector is matched', async () => {
getSingleSignOnConnectorsMock.mockRejectedValueOnce([]);
const { getByText, container } = renderForm([SignInIdentifier.Email], mockSsoConnectors);
const submitButton = getByText('action.create_account');
const emailInput = container.querySelector('input[name="identifier"]');
const termsButton = getByText('description.agree_with_terms');
assert(emailInput, new Error('username input not found'));
act(() => {
fireEvent.change(emailInput, { target: { value: email } });
fireEvent.click(termsButton);
});
act(() => {
fireEvent.submit(submitButton);
});
await waitFor(() => {
expect(getSingleSignOnConnectorsMock).toBeCalled();
expect(sendVerificationCodeApi).toBeCalledWith(UserFlow.Register, {
email,
});
});
});
it('should call check single sign-on connector when the identifier is email, but process to password sign-in if no sso connector is matched', async () => {
getSingleSignOnConnectorsMock.mockResolvedValueOnce(mockSsoConnectors.map(({ id }) => id));
const { getByText, container } = renderForm([SignInIdentifier.Email], mockSsoConnectors);
const submitButton = getByText('action.create_account');
const emailInput = container.querySelector('input[name="identifier"]');
const termsButton = getByText('description.agree_with_terms');
assert(emailInput, new Error('username input not found'));
act(() => {
fireEvent.change(emailInput, { target: { value: email } });
fireEvent.click(termsButton);
});
act(() => {
fireEvent.submit(submitButton);
});
await waitFor(() => {
expect(getSingleSignOnConnectorsMock).toBeCalled();
expect(mockedNavigate).toBeCalledWith(`/${singleSignOnPath}/connectors`);
});
});
});
}); });

View file

@ -1,14 +1,17 @@
import { SignInIdentifier } from '@logto/schemas'; import { SignInIdentifier } from '@logto/schemas';
import { useCallback } from 'react'; import { useCallback } from 'react';
import useCheckSingleSignOn from '@/hooks/use-check-single-sign-on';
import useSendVerificationCode from '@/hooks/use-send-verification-code'; import useSendVerificationCode from '@/hooks/use-send-verification-code';
import { useSieMethods } from '@/hooks/use-sie';
import { UserFlow } from '@/types'; import { UserFlow } from '@/types';
import useRegisterWithUsername from './use-register-with-username'; import useRegisterWithUsername from './use-register-with-username';
// TODO: extract the errorMessage and clear method from useRegisterWithUsername and useSendVerificationCode
const useOnSubmit = () => { const useOnSubmit = () => {
const { ssoConnectors } = useSieMethods();
const { onSubmit: checkSingleSignOn } = useCheckSingleSignOn();
const { const {
errorMessage: usernameRegisterErrorMessage, errorMessage: usernameRegisterErrorMessage,
clearErrorMessage: clearUsernameRegisterErrorMessage, clearErrorMessage: clearUsernameRegisterErrorMessage,
@ -34,9 +37,18 @@ const useOnSubmit = () => {
return; return;
} }
// Check if the email is registered with any SSO connectors. If the email is registered with any SSO connectors, we should not proceed to the next step
if (identifier === SignInIdentifier.Email && ssoConnectors.length > 0) {
const result = await checkSingleSignOn(value);
if (result) {
return;
}
}
await sendVerificationCode({ identifier, value }); await sendVerificationCode({ identifier, value });
}, },
[registerWithUsername, sendVerificationCode] [checkSingleSignOn, registerWithUsername, sendVerificationCode, ssoConnectors.length]
); );
return { return {

View file

@ -1,11 +1,18 @@
import type { SignIn } from '@logto/schemas'; import type { SignIn, SsoConnectorMetadata } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas'; import { SignInIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials'; import { assert } from '@silverhand/essentials';
import { fireEvent, act, waitFor } from '@testing-library/react'; import { fireEvent, act, waitFor } from '@testing-library/react';
import SingleSignOnContextProvider from '@/Providers/SingleSignOnContextProvider';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { mockSignInMethodSettingsTestCases } from '@/__mocks__/logto'; import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import {
mockSignInMethodSettingsTestCases,
mockSignInExperienceSettings,
mockSsoConnectors,
} from '@/__mocks__/logto';
import { sendVerificationCodeApi } from '@/apis/utils'; import { sendVerificationCodeApi } from '@/apis/utils';
import { singleSignOnPath } from '@/constants/env';
import { UserFlow } from '@/types'; import { UserFlow } from '@/types';
import { getDefaultCountryCallingCode } from '@/utils/country-code'; import { getDefaultCountryCallingCode } from '@/utils/country-code';
@ -18,6 +25,7 @@ jest.mock('i18next', () => ({
})); }));
const mockedNavigate = jest.fn(); const mockedNavigate = jest.fn();
const getSingleSignOnConnectorsMock = jest.fn();
jest.mock('@/apis/utils', () => ({ jest.mock('@/apis/utils', () => ({
sendVerificationCodeApi: jest.fn(), sendVerificationCodeApi: jest.fn(),
@ -28,12 +36,27 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedNavigate, useNavigate: () => mockedNavigate,
})); }));
jest.mock('@/apis/single-sign-on', () => ({
getSingleSignOnConnectors: () => getSingleSignOnConnectorsMock(),
}));
const username = 'foo'; const username = 'foo';
const email = 'foo@email.com'; const email = 'foo@email.com';
const phone = '8573333333'; const phone = '8573333333';
const renderForm = (signInMethods: SignIn['methods']) => const renderForm = (signInMethods: SignIn['methods'], ssoConnectors: SsoConnectorMetadata[] = []) =>
renderWithPageContext(<IdentifierSignInForm signInMethods={signInMethods} />); renderWithPageContext(
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
ssoConnectors,
}}
>
<SingleSignOnContextProvider>
<IdentifierSignInForm signInMethods={signInMethods} />
</SingleSignOnContextProvider>
</SettingsProvider>
);
describe('IdentifierSignInForm', () => { describe('IdentifierSignInForm', () => {
afterEach(() => { afterEach(() => {
@ -152,4 +175,116 @@ describe('IdentifierSignInForm', () => {
}); });
} }
); );
describe('email single sign-on tests', () => {
it('should not call check single sign-on connector when the identifier is not email', async () => {
const { getByText, container } = renderForm(
mockSignInMethodSettingsTestCases[0]!,
mockSsoConnectors
);
const inputField = container.querySelector('input[name="identifier"]');
const submitButton = getByText('action.sign_in');
if (inputField) {
act(() => {
fireEvent.change(inputField, { target: { value: username } });
});
}
act(() => {
fireEvent.submit(submitButton);
});
await waitFor(() => {
expect(getSingleSignOnConnectorsMock).not.toBeCalled();
expect(mockedNavigate).toBeCalledWith(
{ pathname: 'password' },
{ state: { identifier: SignInIdentifier.Username, value: username } }
);
});
});
it('should not call check single sign-on connector when no single sign-on connector is enabled', async () => {
const { getByText, container } = renderForm(mockSignInMethodSettingsTestCases[0]!);
const inputField = container.querySelector('input[name="identifier"]');
const submitButton = getByText('action.sign_in');
if (inputField) {
act(() => {
fireEvent.change(inputField, { target: { value: email } });
});
}
act(() => {
fireEvent.submit(submitButton);
});
await waitFor(() => {
expect(getSingleSignOnConnectorsMock).not.toBeCalled();
expect(mockedNavigate).toBeCalledWith(
{ pathname: 'password' },
{ state: { identifier: SignInIdentifier.Email, value: email } }
);
});
});
it('should call check single sign-on connector when the identifier is email, but process to password sign-in if no sso connector is matched', async () => {
getSingleSignOnConnectorsMock.mockRejectedValueOnce([]);
const { getByText, container } = renderForm(
mockSignInMethodSettingsTestCases[0]!,
mockSsoConnectors
);
const inputField = container.querySelector('input[name="identifier"]');
const submitButton = getByText('action.sign_in');
if (inputField) {
act(() => {
fireEvent.change(inputField, { target: { value: email } });
});
}
act(() => {
fireEvent.submit(submitButton);
});
await waitFor(() => {
expect(getSingleSignOnConnectorsMock).toBeCalled();
expect(mockedNavigate).toBeCalledWith(
{ pathname: 'password' },
{ state: { identifier: SignInIdentifier.Email, value: email } }
);
});
});
it('should call check single sign-on connector when the identifier is email, and process to single sign-on if a sso connector is matched', async () => {
getSingleSignOnConnectorsMock.mockResolvedValueOnce(mockSsoConnectors.map(({ id }) => id));
const { getByText, container } = renderForm(
mockSignInMethodSettingsTestCases[0]!,
mockSsoConnectors
);
const inputField = container.querySelector('input[name="identifier"]');
const submitButton = getByText('action.sign_in');
if (inputField) {
act(() => {
fireEvent.change(inputField, { target: { value: email } });
});
}
act(() => {
fireEvent.submit(submitButton);
});
await waitFor(() => {
expect(getSingleSignOnConnectorsMock).toBeCalled();
expect(mockedNavigate).toBeCalledWith(`/${singleSignOnPath}/connectors`);
});
});
});
}); });

View file

@ -3,11 +3,15 @@ import { SignInIdentifier } from '@logto/schemas';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import useCheckSingleSignOn from '@/hooks/use-check-single-sign-on';
import useSendVerificationCode from '@/hooks/use-send-verification-code'; import useSendVerificationCode from '@/hooks/use-send-verification-code';
import { useSieMethods } from '@/hooks/use-sie';
import { UserFlow } from '@/types'; import { UserFlow } from '@/types';
const useOnSubmit = (signInMethods: SignIn['methods']) => { const useOnSubmit = (signInMethods: SignIn['methods']) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { ssoConnectors } = useSieMethods();
const { onSubmit: checkSingleSignOn } = useCheckSingleSignOn();
const signInWithPassword = useCallback( const signInWithPassword = useCallback(
(identifier: SignInIdentifier, value: string) => { (identifier: SignInIdentifier, value: string) => {
@ -27,31 +31,49 @@ const useOnSubmit = (signInMethods: SignIn['methods']) => {
onSubmit: sendVerificationCode, onSubmit: sendVerificationCode,
} = useSendVerificationCode(UserFlow.SignIn); } = useSendVerificationCode(UserFlow.SignIn);
const onSubmit = async (identifier: SignInIdentifier, value: string) => { const onSubmit = useCallback(
const method = signInMethods.find((method) => method.identifier === identifier); async (identifier: SignInIdentifier, value: string) => {
const method = signInMethods.find((method) => method.identifier === identifier);
if (!method) { if (!method) {
throw new Error(`Cannot find method with identifier type ${identifier}`); throw new Error(`Cannot find method with identifier type ${identifier}`);
} }
const { password, isPasswordPrimary, verificationCode } = method; const { password, isPasswordPrimary, verificationCode } = method;
if (identifier === SignInIdentifier.Username) { if (identifier === SignInIdentifier.Username) {
signInWithPassword(identifier, value); signInWithPassword(identifier, value);
return; return;
} }
if (password && (isPasswordPrimary || !verificationCode)) { // Check if the email is registered with any SSO connectors. If the email is registered with any SSO connectors, we should not proceed to the next step
signInWithPassword(identifier, value); if (identifier === SignInIdentifier.Email && ssoConnectors.length > 0) {
const result = await checkSingleSignOn(value);
return; if (result) {
} return;
}
}
if (verificationCode) { if (password && (isPasswordPrimary || !verificationCode)) {
await sendVerificationCode({ identifier, value }); signInWithPassword(identifier, value);
}
}; return;
}
if (verificationCode) {
await sendVerificationCode({ identifier, value });
}
},
[
checkSingleSignOn,
sendVerificationCode,
signInMethods,
signInWithPassword,
ssoConnectors.length,
]
);
return { return {
errorMessage, errorMessage,

View file

@ -9,10 +9,10 @@ import ErrorMessage from '@/components/ErrorMessage';
import SmartInputField, { import SmartInputField, {
type IdentifierInputValue, type IdentifierInputValue,
} from '@/components/InputFields/SmartInputField'; } from '@/components/InputFields/SmartInputField';
import useOnSubmit from '@/hooks/use-check-single-sign-on';
import { getGeneralIdentifierErrorMessage, validateIdentifierField } from '@/utils/form'; import { getGeneralIdentifierErrorMessage, validateIdentifierField } from '@/utils/form';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
import useOnSubmit from './use-on-submit';
type FormState = { type FormState = {
identifier: IdentifierInputValue; identifier: IdentifierInputValue;