0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

fix(core): should not guard email if SSO is disabled (#5032)

should not guard email is SSO is disabled
This commit is contained in:
simeng-li 2023-12-01 14:56:59 +08:00 committed by GitHub
parent 61d1894fa9
commit 997a0036d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 482 additions and 177 deletions

View file

@ -83,7 +83,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
if (identifier && event !== InteractionEvent.ForgotPassword) { if (identifier && event !== InteractionEvent.ForgotPassword) {
verifyIdentifierSettings(identifier, signInExperience); verifyIdentifierSettings(identifier, signInExperience);
await verifySsoOnlyEmailIdentifier(libraries.ssoConnectors, identifier); await verifySsoOnlyEmailIdentifier(libraries.ssoConnectors, identifier, signInExperience);
} }
if (profile && event !== InteractionEvent.ForgotPassword) { if (profile && event !== InteractionEvent.ForgotPassword) {
@ -183,7 +183,11 @@ export default function interactionRoutes<T extends AnonymousRouter>(
if (interactionStorage.event !== InteractionEvent.ForgotPassword) { if (interactionStorage.event !== InteractionEvent.ForgotPassword) {
verifyIdentifierSettings(identifierPayload, signInExperience); verifyIdentifierSettings(identifierPayload, signInExperience);
await verifySsoOnlyEmailIdentifier(libraries.ssoConnectors, identifierPayload); await verifySsoOnlyEmailIdentifier(
libraries.ssoConnectors,
identifierPayload,
signInExperience
);
} }
const verifiedIdentifier = await verifyIdentifierPayload( const verifiedIdentifier = await verifyIdentifierPayload(

View file

@ -1,3 +1,4 @@
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
import { wellConfiguredSsoConnector } from '#src/__mocks__/sso.js'; import { wellConfiguredSsoConnector } from '#src/__mocks__/sso.js';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
import { type SsoConnectorLibrary } from '#src/libraries/sso-connector.js'; import { type SsoConnectorLibrary } from '#src/libraries/sso-connector.js';
@ -15,14 +16,41 @@ const mockSsoConnectorLibrary: SsoConnectorLibrary = {
}; };
describe('verifyEmailIdentifier tests', () => { describe('verifyEmailIdentifier tests', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should return if the identifier is not an email', async () => { it('should return if the identifier is not an email', async () => {
await expect( await expect(
verifySsoOnlyEmailIdentifier(mockSsoConnectorLibrary, { verifySsoOnlyEmailIdentifier(
username: 'foo', mockSsoConnectorLibrary,
password: 'bar', {
}) username: 'foo',
password: 'bar',
},
mockSignInExperience
)
).resolves.not.toThrow(); ).resolves.not.toThrow();
}); });
it('should return if the ssoConnector is not enabled', async () => {
await expect(
verifySsoOnlyEmailIdentifier(
mockSsoConnectorLibrary,
{
email: 'foo@bar.com',
password: 'bar',
},
{
...mockSignInExperience,
singleSignOnEnabled: false,
}
)
).resolves.not.toThrow();
expect(getAvailableSsoConnectorsMock).not.toBeCalled();
});
it('should return if no sso connectors found with the given email', async () => { it('should return if no sso connectors found with the given email', async () => {
getAvailableSsoConnectorsMock.mockResolvedValueOnce([ getAvailableSsoConnectorsMock.mockResolvedValueOnce([
{ {
@ -32,10 +60,14 @@ describe('verifyEmailIdentifier tests', () => {
]); ]);
await expect( await expect(
verifySsoOnlyEmailIdentifier(mockSsoConnectorLibrary, { verifySsoOnlyEmailIdentifier(
email: 'foo@bar.com', mockSsoConnectorLibrary,
password: 'bar', {
}) email: 'foo@bar.com',
password: 'bar',
},
mockSignInExperience
)
).resolves.not.toThrow(); ).resolves.not.toThrow();
}); });
@ -48,10 +80,14 @@ describe('verifyEmailIdentifier tests', () => {
getAvailableSsoConnectorsMock.mockResolvedValueOnce([connector]); getAvailableSsoConnectorsMock.mockResolvedValueOnce([connector]);
await expect( await expect(
verifySsoOnlyEmailIdentifier(mockSsoConnectorLibrary, { verifySsoOnlyEmailIdentifier(
email: 'foo@example.com', mockSsoConnectorLibrary,
verificationCode: 'bar', {
}) email: 'foo@example.com',
verificationCode: 'bar',
},
mockSignInExperience
)
).rejects.toMatchError( ).rejects.toMatchError(
new RequestError( new RequestError(
{ {

View file

@ -1,5 +1,5 @@
import { type SocialUserInfo } from '@logto/connector-kit'; import { type SocialUserInfo } from '@logto/connector-kit';
import { type IdentifierPayload } from '@logto/schemas'; import { type IdentifierPayload, type SignInExperience } from '@logto/schemas';
import { EnvSet } from '#src/env-set/index.js'; import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
@ -9,7 +9,8 @@ import assertThat from '#src/utils/assert-that.js';
// Guard the SSO only email identifier // Guard the SSO only email identifier
export const verifySsoOnlyEmailIdentifier = async ( export const verifySsoOnlyEmailIdentifier = async (
{ getAvailableSsoConnectors }: SsoConnectorLibrary, { getAvailableSsoConnectors }: SsoConnectorLibrary,
identifier: IdentifierPayload | SocialUserInfo identifier: IdentifierPayload | SocialUserInfo,
signInExperience: SignInExperience
) => { ) => {
// TODO: @simeng-li remove the dev features check when the SSO feature is released // TODO: @simeng-li remove the dev features check when the SSO feature is released
if (!EnvSet.values.isDevFeaturesEnabled) { if (!EnvSet.values.isDevFeaturesEnabled) {
@ -20,6 +21,11 @@ export const verifySsoOnlyEmailIdentifier = async (
return; return;
} }
// SSO is not enabled
if (!signInExperience.singleSignOnEnabled) {
return;
}
const { email } = identifier; const { email } = identifier;
const availableSsoConnectors = await getAvailableSsoConnectors(); const availableSsoConnectors = await getAvailableSsoConnectors();
const domain = email.split('@')[1]; const domain = email.split('@')[1];

View file

@ -1,6 +1,10 @@
import crypto from 'node:crypto';
import { PasswordPolicyChecker } from '@logto/core-kit';
import { InteractionEvent } from '@logto/schemas'; import { InteractionEvent } from '@logto/schemas';
import { createMockUtils, pickDefault } from '@logto/shared/esm'; import { createMockUtils, pickDefault } from '@logto/shared/esm';
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
import { createMockLogContext } from '#src/test-utils/koa-audit-log.js'; import { createMockLogContext } from '#src/test-utils/koa-audit-log.js';
import { MockTenant } from '#src/test-utils/tenant.js'; import { MockTenant } from '#src/test-utils/tenant.js';
@ -44,7 +48,16 @@ const logContext = createMockLogContext();
const tenant = new MockTenant(); const tenant = new MockTenant();
describe('identifier verification', () => { describe('identifier verification', () => {
const baseCtx = { ...createContextWithRouteParameters(), ...logContext }; const baseCtx = {
...createContextWithRouteParameters(),
...logContext,
signInExperience: mockSignInExperience,
passwordPolicyChecker: new PasswordPolicyChecker(
mockSignInExperience.passwordPolicy,
crypto.subtle
),
};
const interactionStorage = { event: InteractionEvent.SignIn }; const interactionStorage = { event: InteractionEvent.SignIn };
afterEach(() => { afterEach(() => {
@ -194,7 +207,11 @@ describe('identifier verification', () => {
).rejects.toMatchError(new RequestError('session.sso_enabled')); ).rejects.toMatchError(new RequestError('session.sso_enabled'));
expect(verifySocialIdentity).toBeCalledWith(identifier, baseCtx, tenant); expect(verifySocialIdentity).toBeCalledWith(identifier, baseCtx, tenant);
expect(verifySsoOnlyEmailIdentifier).toBeCalledWith(tenant.libraries.ssoConnectors, useInfo); expect(verifySsoOnlyEmailIdentifier).toBeCalledWith(
tenant.libraries.ssoConnectors,
useInfo,
mockSignInExperience
);
expect(findUserByIdentifier).not.toBeCalled(); expect(findUserByIdentifier).not.toBeCalled();
}); });

View file

@ -18,6 +18,7 @@ import type TenantContext from '#src/tenants/TenantContext.js';
import assertThat from '#src/utils/assert-that.js'; import assertThat from '#src/utils/assert-that.js';
import { i18next } from '#src/utils/i18n.js'; import { i18next } from '#src/utils/i18n.js';
import { type WithInteractionSieContext } from '../middleware/koa-interaction-sie.js';
import type { import type {
PasswordIdentifierPayload, PasswordIdentifierPayload,
SocialIdentifier, SocialIdentifier,
@ -86,7 +87,7 @@ const verifyVerificationCodeIdentifier = async (
const verifySocialIdentifier = async ( const verifySocialIdentifier = async (
identifier: SocialConnectorPayload, identifier: SocialConnectorPayload,
ctx: WithLogContext, ctx: WithInteractionSieContext<WithLogContext>,
tenant: TenantContext tenant: TenantContext
): Promise<SocialIdentifier> => { ): Promise<SocialIdentifier> => {
const userInfo = await verifySocialIdentity(identifier, ctx, tenant); const userInfo = await verifySocialIdentity(identifier, ctx, tenant);
@ -94,7 +95,10 @@ const verifySocialIdentifier = async (
const { const {
libraries: { ssoConnectors }, libraries: { ssoConnectors },
} = tenant; } = tenant;
await verifySsoOnlyEmailIdentifier(ssoConnectors, userInfo);
const { signInExperience } = ctx;
await verifySsoOnlyEmailIdentifier(ssoConnectors, userInfo, signInExperience);
return { key: 'social', connectorId: identifier.connectorId, userInfo }; return { key: 'social', connectorId: identifier.connectorId, userInfo };
}; };
@ -158,7 +162,7 @@ const verifySocialVerifiedIdentifier = async (
* or phone, then the payload is valid. * or phone, then the payload is valid.
*/ */
async function identifierPayloadVerification( async function identifierPayloadVerification(
ctx: WithLogContext, ctx: WithInteractionSieContext<WithLogContext>,
tenant: TenantContext, tenant: TenantContext,
identifierPayload: IdentifierPayload, identifierPayload: IdentifierPayload,
interactionStorage: AnonymousInteractionResult interactionStorage: AnonymousInteractionResult

View file

@ -13,7 +13,7 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
import type { IdentifierVerifiedInteractionResult } from '../types/index.js'; import type { IdentifierVerifiedInteractionResult } from '../types/index.js';
const { jest } = import.meta; const { jest } = import.meta;
const { mockEsm, mockEsmWithActual } = createMockUtils(jest); const { mockEsm } = createMockUtils(jest);
const findUserById = jest.fn(); const findUserById = jest.fn();
const hasUserWithEmail = jest.fn(); const hasUserWithEmail = jest.fn();

View file

@ -6,8 +6,6 @@ import {
sendVerificationCode, sendVerificationCode,
} from '#src/api/interaction.js'; } from '#src/api/interaction.js';
import { updateSignInExperience } from '#src/api/sign-in-experience.js'; import { updateSignInExperience } from '#src/api/sign-in-experience.js';
import { createSsoConnector } from '#src/api/sso-connector.js';
import { newOidcSsoConnectorPayload } from '#src/constants.js';
import { initClient } from '#src/helpers/client.js'; import { initClient } from '#src/helpers/client.js';
import { import {
clearConnectorsByTypes, clearConnectorsByTypes,
@ -16,17 +14,9 @@ import {
setSmsConnector, setSmsConnector,
} from '#src/helpers/connector.js'; } from '#src/helpers/connector.js';
import { expectRejects, readVerificationCode } from '#src/helpers/index.js'; import { expectRejects, readVerificationCode } from '#src/helpers/index.js';
import { import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
enableAllPasswordSignInMethods,
enableAllVerificationCodeSignInMethods,
} from '#src/helpers/sign-in-experience.js';
import { generateNewUserProfile } from '#src/helpers/user.js'; import { generateNewUserProfile } from '#src/helpers/user.js';
import { import { generatePassword, generateUsername } from '#src/utils.js';
generateEmail,
generatePassword,
generateSsoConnectorName,
generateUsername,
} from '#src/utils.js';
describe('Register with identifiers sad path', () => { describe('Register with identifiers sad path', () => {
beforeAll(async () => { beforeAll(async () => {
@ -58,40 +48,6 @@ describe('Register with identifiers sad path', () => {
await updateSignInExperience({ signInMode: SignInMode.SignInAndRegister }); await updateSignInExperience({ signInMode: SignInMode.SignInAndRegister });
}); });
it('Should fail to register with email if email domain is enabled for SSO only', async () => {
await enableAllVerificationCodeSignInMethods();
const email = generateEmail('sso-register-sad-path.io');
await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-register-sad-path.io'],
});
const client = await initClient();
await client.successSend(putInteraction, {
event: InteractionEvent.Register,
});
await client.successSend(sendVerificationCode, {
email,
});
const { code: verificationCode } = await readVerificationCode();
await expectRejects(
client.send(patchInteractionIdentifiers, {
email,
verificationCode,
}),
{
code: 'session.sso_enabled',
statusCode: 422,
}
);
});
describe('Should fail to register with identifiers if sign-up settings are not enabled', () => { describe('Should fail to register with identifiers if sign-up settings are not enabled', () => {
beforeAll(async () => { beforeAll(async () => {
// This function call will disable all sign-up settings by default // This function call will disable all sign-up settings by default

View file

@ -0,0 +1,140 @@
import { ConnectorType, InteractionEvent, SignInIdentifier } from '@logto/schemas';
import { deleteUser } from '#src/api/admin-user.js';
import {
putInteraction,
sendVerificationCode,
patchInteractionIdentifiers,
putInteractionProfile,
} from '#src/api/interaction.js';
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
import { createSsoConnector, deleteSsoConnectorById } from '#src/api/sso-connector.js';
import { newOidcSsoConnectorPayload } from '#src/constants.js';
import { initClient, processSession, logoutClient } from '#src/helpers/client.js';
import {
clearConnectorsByTypes,
clearSsoConnectors,
setEmailConnector,
setSmsConnector,
} from '#src/helpers/connector.js';
import { expectRejects, readVerificationCode } from '#src/helpers/index.js';
import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js';
import { generateEmail, generateSsoConnectorName } from '#src/utils.js';
const happyPath = async (email: string) => {
const client = await initClient();
await client.successSend(putInteraction, {
event: InteractionEvent.Register,
});
await client.successSend(sendVerificationCode, {
email,
});
const verificationCodeRecord = await readVerificationCode();
expect(verificationCodeRecord).toMatchObject({
address: email,
type: InteractionEvent.Register,
});
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
email,
verificationCode: code,
});
await client.successSend(putInteractionProfile, {
email,
});
const { redirectTo } = await client.submitInteraction();
const id = await processSession(client, redirectTo);
await logoutClient(client);
await deleteUser(id);
};
describe('test register with email with SSO feature', () => {
beforeAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await setEmailConnector();
await setSmsConnector();
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Email],
password: false,
verify: true,
});
});
afterAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email]);
await clearSsoConnectors();
});
it('should register with email with SSO disabled', async () => {
await updateSignInExperience({ singleSignOnEnabled: false });
const email = generateEmail('sso-register-happy-path.io');
const { id } = await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-register-happy-path.io'],
});
await happyPath(email);
await deleteSsoConnectorById(id);
});
it('should register with email with SSO enabled but no connector found', async () => {
await updateSignInExperience({ singleSignOnEnabled: true });
const email = generateEmail('sso-register-happy-path.io');
const { id } = await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['happy.io'],
});
await happyPath(email);
await deleteSsoConnectorById(id);
});
it('Should fail to register with email if email domain is enabled for SSO only', async () => {
await updateSignInExperience({ singleSignOnEnabled: true });
const email = generateEmail('sso-register-sad-path.io');
await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-register-sad-path.io'],
});
const client = await initClient();
await client.successSend(putInteraction, {
event: InteractionEvent.Register,
});
await client.successSend(sendVerificationCode, {
email,
});
const { code: verificationCode } = await readVerificationCode();
await expectRejects(
client.send(patchInteractionIdentifiers, {
email,
verificationCode,
}),
{
code: 'session.sso_enabled',
statusCode: 422,
}
);
});
});

View file

@ -1,15 +1,13 @@
import { ConnectorType } from '@logto/connector-kit'; import { ConnectorType } from '@logto/connector-kit';
import { InteractionEvent, SignInMode } from '@logto/schemas'; import { InteractionEvent, SignInMode } from '@logto/schemas';
import { createUser, deleteUser, suspendUser } from '#src/api/admin-user.js'; import { suspendUser } from '#src/api/admin-user.js';
import { import {
patchInteractionIdentifiers, patchInteractionIdentifiers,
putInteraction, putInteraction,
sendVerificationCode, sendVerificationCode,
} from '#src/api/interaction.js'; } from '#src/api/interaction.js';
import { updateSignInExperience } from '#src/api/sign-in-experience.js'; import { updateSignInExperience } from '#src/api/sign-in-experience.js';
import { createSsoConnector } from '#src/api/sso-connector.js';
import { newOidcSsoConnectorPayload } from '#src/constants.js';
import { initClient } from '#src/helpers/client.js'; import { initClient } from '#src/helpers/client.js';
import { import {
clearConnectorsByTypes, clearConnectorsByTypes,
@ -20,12 +18,7 @@ import {
import { expectRejects, readVerificationCode } from '#src/helpers/index.js'; import { expectRejects, readVerificationCode } from '#src/helpers/index.js';
import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js'; import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js';
import { generateNewUser } from '#src/helpers/user.js'; import { generateNewUser } from '#src/helpers/user.js';
import { import { generateEmail, generatePhone } from '#src/utils.js';
generateEmail,
generatePassword,
generatePhone,
generateSsoConnectorName,
} from '#src/utils.js';
describe('Sign-in flow sad path using verification-code identifiers', () => { describe('Sign-in flow sad path using verification-code identifiers', () => {
beforeAll(async () => { beforeAll(async () => {
@ -218,42 +211,6 @@ describe('Sign-in flow sad path using verification-code identifiers', () => {
}); });
}); });
it('Should fail to sign in with email and passcode if the email domain is enabled for SSO only', async () => {
const password = generatePassword();
const email = generateEmail('sso-sad-path.io');
const user = await createUser({ primaryEmail: email, password });
await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-sad-path.io'],
});
const client = await initClient();
await client.successSend(putInteraction, {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationCode, {
email,
});
const { code: verificationCode } = await readVerificationCode();
await expectRejects(
client.send(patchInteractionIdentifiers, {
email,
verificationCode,
}),
{
code: 'session.sso_enabled',
statusCode: 422,
}
);
await deleteUser(user.id);
});
it('Should fail to sign in with phone and passcode if related user is not exist', async () => { it('Should fail to sign in with phone and passcode if related user is not exist', async () => {
const notExistUserPhone = generatePhone(); const notExistUserPhone = generatePhone();

View file

@ -0,0 +1,140 @@
import { ConnectorType, InteractionEvent, SignInIdentifier } from '@logto/schemas';
import { createUser, deleteUser } from '#src/api/admin-user.js';
import {
putInteraction,
sendVerificationCode,
patchInteractionIdentifiers,
} from '#src/api/interaction.js';
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
import { createSsoConnector, deleteSsoConnectorById } from '#src/api/sso-connector.js';
import { newOidcSsoConnectorPayload } from '#src/constants.js';
import { initClient, processSession, logoutClient } from '#src/helpers/client.js';
import {
clearConnectorsByTypes,
clearSsoConnectors,
setEmailConnector,
setSmsConnector,
} from '#src/helpers/connector.js';
import { expectRejects, readVerificationCode } from '#src/helpers/index.js';
import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js';
import { generateEmail, generateSsoConnectorName } from '#src/utils.js';
const happyPath = async (email: string) => {
const user = await createUser({ primaryEmail: email });
const client = await initClient();
await client.successSend(putInteraction, {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationCode, {
email,
});
const verificationCodeRecord = await readVerificationCode();
expect(verificationCodeRecord).toMatchObject({
address: email,
type: InteractionEvent.SignIn,
});
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
email,
verificationCode: code,
});
const { redirectTo } = await client.submitInteraction();
await processSession(client, redirectTo);
await logoutClient(client);
await deleteUser(user.id);
};
describe('test sign-in with email passcode identifier with SSO feature', () => {
beforeAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await setEmailConnector();
await setSmsConnector();
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Email],
password: false,
verify: true,
});
});
afterAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email]);
await clearSsoConnectors();
});
it('Should fail to sign in with email and passcode if the email domain is enabled for SSO only', async () => {
const email = generateEmail('sso-sad-path.io');
const user = await createUser({ primaryEmail: email });
await updateSignInExperience({ singleSignOnEnabled: true });
const { id } = await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-sad-path.io'],
});
const client = await initClient();
await client.successSend(putInteraction, {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationCode, {
email,
});
const { code: verificationCode } = await readVerificationCode();
await expectRejects(
client.send(patchInteractionIdentifiers, {
email,
verificationCode,
}),
{
code: 'session.sso_enabled',
statusCode: 422,
}
);
await deleteUser(user.id);
await deleteSsoConnectorById(id);
});
it('Should sign-in with email with SSO disabled', async () => {
await updateSignInExperience({ singleSignOnEnabled: false });
const { id } = await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-sign-in-happy-path.io'],
});
const email = generateEmail('sso-sign-in-happy-path.io');
await happyPath(email);
await deleteSsoConnectorById(id);
});
it('Should sign-in with email with SSO enabled but no connector found', async () => {
await updateSignInExperience({ singleSignOnEnabled: true });
const { id } = await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-sign-in-happy-path.io'],
});
const email = generateEmail('happy-path.io');
await happyPath(email);
await deleteSsoConnectorById(id);
});
});

View file

@ -7,8 +7,6 @@ import {
putInteractionProfile, putInteractionProfile,
deleteUser, deleteUser,
} from '#src/api/index.js'; } from '#src/api/index.js';
import { createSsoConnector } from '#src/api/sso-connector.js';
import { newOidcSsoConnectorPayload } from '#src/constants.js';
import { initClient, processSession, logoutClient } from '#src/helpers/client.js'; import { initClient, processSession, logoutClient } from '#src/helpers/client.js';
import { import {
clearConnectorsByTypes, clearConnectorsByTypes,
@ -21,7 +19,6 @@ import {
enableAllVerificationCodeSignInMethods, enableAllVerificationCodeSignInMethods,
} from '#src/helpers/sign-in-experience.js'; } from '#src/helpers/sign-in-experience.js';
import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js'; import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js';
import { generateSsoConnectorName } from '#src/utils.js';
describe('Sign-in flow using password identifiers', () => { describe('Sign-in flow using password identifiers', () => {
beforeAll(async () => { beforeAll(async () => {
@ -75,32 +72,6 @@ describe('Sign-in flow using password identifiers', () => {
await deleteUser(user.id); await deleteUser(user.id);
}); });
it('should allow sign-in with email and password with unmatched SSO connector domains', async () => {
const { userProfile, user } = await generateNewUser({ primaryEmail: true, password: true });
const client = await initClient();
// Create a new OIDC SSO connector with email domain 'example.com', it should not block the sign-in flow of email logto.io
await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
});
await client.successSend(putInteraction, {
event: InteractionEvent.SignIn,
identifier: {
email: userProfile.primaryEmail,
password: userProfile.password,
},
});
const { redirectTo } = await client.submitInteraction();
await processSession(client, redirectTo);
await logoutClient(client);
await deleteUser(user.id);
});
it('sign-in with phone and password', async () => { it('sign-in with phone and password', async () => {
const { userProfile, user } = await generateNewUser({ primaryPhone: true, password: true }); const { userProfile, user } = await generateNewUser({ primaryPhone: true, password: true });
const client = await initClient(); const client = await initClient();

View file

@ -1,21 +1,14 @@
import { InteractionEvent, SignInMode } from '@logto/schemas'; import { InteractionEvent, SignInMode } from '@logto/schemas';
import { createUser, deleteUser, suspendUser } from '#src/api/admin-user.js'; import { suspendUser } from '#src/api/admin-user.js';
import { putInteraction } from '#src/api/interaction.js'; import { putInteraction } from '#src/api/interaction.js';
import { updateSignInExperience } from '#src/api/sign-in-experience.js'; import { updateSignInExperience } from '#src/api/sign-in-experience.js';
import { createSsoConnector } from '#src/api/sso-connector.js';
import { newOidcSsoConnectorPayload } from '#src/constants.js';
import { initClient } from '#src/helpers/client.js'; import { initClient } from '#src/helpers/client.js';
import { clearSsoConnectors } from '#src/helpers/connector.js'; import { clearSsoConnectors } from '#src/helpers/connector.js';
import { expectRejects } from '#src/helpers/index.js'; import { expectRejects } from '#src/helpers/index.js';
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
import { generateNewUser } from '#src/helpers/user.js'; import { generateNewUser } from '#src/helpers/user.js';
import { import { generateName, generatePassword } from '#src/utils.js';
generateEmail,
generateName,
generatePassword,
generateSsoConnectorName,
} from '#src/utils.js';
describe('Sign-in flow sad path using password identifiers', () => { describe('Sign-in flow sad path using password identifiers', () => {
beforeAll(async () => { beforeAll(async () => {
@ -189,32 +182,6 @@ describe('Sign-in flow sad path using password identifiers', () => {
); );
}); });
it('Should fail to sign-in with email and password if the email domain is enabled for SSO only', async () => {
const password = generatePassword();
const email = generateEmail('sso-sad-path.io');
const user = await createUser({ primaryEmail: email, password });
await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-sad-path.io'],
});
const client = await initClient();
await expectRejects(
client.send(putInteraction, {
event: InteractionEvent.SignIn,
identifier: { email, password },
}),
{
code: 'session.sso_enabled',
statusCode: 422,
}
);
await deleteUser(user.id);
});
it('Should fail to sign-in with username and password if the user is suspended', async () => { it('Should fail to sign-in with username and password if the user is suspended', async () => {
const { const {
user, user,

View file

@ -0,0 +1,107 @@
import { ConnectorType, InteractionEvent } from '@logto/schemas';
import { createUser, deleteUser } from '#src/api/admin-user.js';
import { putInteraction } from '#src/api/interaction.js';
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
import { createSsoConnector, deleteSsoConnectorById } from '#src/api/sso-connector.js';
import { newOidcSsoConnectorPayload } from '#src/constants.js';
import { initClient, processSession, logoutClient } from '#src/helpers/client.js';
import {
clearConnectorsByTypes,
clearSsoConnectors,
setEmailConnector,
setSmsConnector,
} from '#src/helpers/connector.js';
import { expectRejects } from '#src/helpers/index.js';
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
import { generateEmail, generatePassword, generateSsoConnectorName } from '#src/utils.js';
const happyPath = async (email: string) => {
const password = generatePassword();
const user = await createUser({ primaryEmail: email, password });
const client = await initClient();
await client.successSend(putInteraction, {
event: InteractionEvent.SignIn,
identifier: {
email,
password,
},
});
const { redirectTo } = await client.submitInteraction();
await processSession(client, redirectTo);
await logoutClient(client);
await deleteUser(user.id);
};
describe('test sign-in with email passcode identifier with SSO feature', () => {
beforeAll(async () => {
await enableAllPasswordSignInMethods();
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await setEmailConnector();
await setSmsConnector();
});
afterAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await clearSsoConnectors();
});
it('should allow sign-in with email and password with SSO disabled', async () => {
await updateSignInExperience({ singleSignOnEnabled: false });
const email = generateEmail('sso-password-happy-path.io');
const { id } = await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-password-happy-path.io'],
});
await happyPath(email);
await deleteSsoConnectorById(id);
});
it('should allow sign-in with email and password with unmatched SSO connector domains', async () => {
await updateSignInExperience({ singleSignOnEnabled: true });
const email = generateEmail('happy-path.io');
const { id } = await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-password-happy-path.io'],
});
await happyPath(email);
await deleteSsoConnectorById(id);
});
it('Should fail to sign-in with email and password if the email domain is enabled for SSO only', async () => {
const password = generatePassword();
const email = generateEmail('sso-sad-path.io');
const user = await createUser({ primaryEmail: email, password });
await createSsoConnector({
...newOidcSsoConnectorPayload,
connectorName: generateSsoConnectorName(),
domains: ['sso-sad-path.io'],
});
const client = await initClient();
await expectRejects(
client.send(putInteraction, {
event: InteractionEvent.SignIn,
identifier: { email, password },
}),
{
code: 'session.sso_enabled',
statusCode: 422,
}
);
await deleteUser(user.id);
});
});