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:
parent
61d1894fa9
commit
997a0036d8
13 changed files with 482 additions and 177 deletions
|
@ -83,7 +83,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
|
||||
if (identifier && event !== InteractionEvent.ForgotPassword) {
|
||||
verifyIdentifierSettings(identifier, signInExperience);
|
||||
await verifySsoOnlyEmailIdentifier(libraries.ssoConnectors, identifier);
|
||||
await verifySsoOnlyEmailIdentifier(libraries.ssoConnectors, identifier, signInExperience);
|
||||
}
|
||||
|
||||
if (profile && event !== InteractionEvent.ForgotPassword) {
|
||||
|
@ -183,7 +183,11 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
|
||||
if (interactionStorage.event !== InteractionEvent.ForgotPassword) {
|
||||
verifyIdentifierSettings(identifierPayload, signInExperience);
|
||||
await verifySsoOnlyEmailIdentifier(libraries.ssoConnectors, identifierPayload);
|
||||
await verifySsoOnlyEmailIdentifier(
|
||||
libraries.ssoConnectors,
|
||||
identifierPayload,
|
||||
signInExperience
|
||||
);
|
||||
}
|
||||
|
||||
const verifiedIdentifier = await verifyIdentifierPayload(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
|
||||
import { wellConfiguredSsoConnector } from '#src/__mocks__/sso.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type SsoConnectorLibrary } from '#src/libraries/sso-connector.js';
|
||||
|
@ -15,14 +16,41 @@ const mockSsoConnectorLibrary: SsoConnectorLibrary = {
|
|||
};
|
||||
|
||||
describe('verifyEmailIdentifier tests', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return if the identifier is not an email', async () => {
|
||||
await expect(
|
||||
verifySsoOnlyEmailIdentifier(mockSsoConnectorLibrary, {
|
||||
username: 'foo',
|
||||
password: 'bar',
|
||||
})
|
||||
verifySsoOnlyEmailIdentifier(
|
||||
mockSsoConnectorLibrary,
|
||||
{
|
||||
username: 'foo',
|
||||
password: 'bar',
|
||||
},
|
||||
mockSignInExperience
|
||||
)
|
||||
).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 () => {
|
||||
getAvailableSsoConnectorsMock.mockResolvedValueOnce([
|
||||
{
|
||||
|
@ -32,10 +60,14 @@ describe('verifyEmailIdentifier tests', () => {
|
|||
]);
|
||||
|
||||
await expect(
|
||||
verifySsoOnlyEmailIdentifier(mockSsoConnectorLibrary, {
|
||||
email: 'foo@bar.com',
|
||||
password: 'bar',
|
||||
})
|
||||
verifySsoOnlyEmailIdentifier(
|
||||
mockSsoConnectorLibrary,
|
||||
{
|
||||
email: 'foo@bar.com',
|
||||
password: 'bar',
|
||||
},
|
||||
mockSignInExperience
|
||||
)
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
|
@ -48,10 +80,14 @@ describe('verifyEmailIdentifier tests', () => {
|
|||
getAvailableSsoConnectorsMock.mockResolvedValueOnce([connector]);
|
||||
|
||||
await expect(
|
||||
verifySsoOnlyEmailIdentifier(mockSsoConnectorLibrary, {
|
||||
email: 'foo@example.com',
|
||||
verificationCode: 'bar',
|
||||
})
|
||||
verifySsoOnlyEmailIdentifier(
|
||||
mockSsoConnectorLibrary,
|
||||
{
|
||||
email: 'foo@example.com',
|
||||
verificationCode: 'bar',
|
||||
},
|
||||
mockSignInExperience
|
||||
)
|
||||
).rejects.toMatchError(
|
||||
new RequestError(
|
||||
{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 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
|
||||
export const verifySsoOnlyEmailIdentifier = async (
|
||||
{ getAvailableSsoConnectors }: SsoConnectorLibrary,
|
||||
identifier: IdentifierPayload | SocialUserInfo
|
||||
identifier: IdentifierPayload | SocialUserInfo,
|
||||
signInExperience: SignInExperience
|
||||
) => {
|
||||
// TODO: @simeng-li remove the dev features check when the SSO feature is released
|
||||
if (!EnvSet.values.isDevFeaturesEnabled) {
|
||||
|
@ -20,6 +21,11 @@ export const verifySsoOnlyEmailIdentifier = async (
|
|||
return;
|
||||
}
|
||||
|
||||
// SSO is not enabled
|
||||
if (!signInExperience.singleSignOnEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { email } = identifier;
|
||||
const availableSsoConnectors = await getAvailableSsoConnectors();
|
||||
const domain = email.split('@')[1];
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import crypto from 'node:crypto';
|
||||
|
||||
import { PasswordPolicyChecker } from '@logto/core-kit';
|
||||
import { InteractionEvent } from '@logto/schemas';
|
||||
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 { createMockLogContext } from '#src/test-utils/koa-audit-log.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
|
@ -44,7 +48,16 @@ const logContext = createMockLogContext();
|
|||
const tenant = new MockTenant();
|
||||
|
||||
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 };
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -194,7 +207,11 @@ describe('identifier verification', () => {
|
|||
).rejects.toMatchError(new RequestError('session.sso_enabled'));
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import type TenantContext from '#src/tenants/TenantContext.js';
|
|||
import assertThat from '#src/utils/assert-that.js';
|
||||
import { i18next } from '#src/utils/i18n.js';
|
||||
|
||||
import { type WithInteractionSieContext } from '../middleware/koa-interaction-sie.js';
|
||||
import type {
|
||||
PasswordIdentifierPayload,
|
||||
SocialIdentifier,
|
||||
|
@ -86,7 +87,7 @@ const verifyVerificationCodeIdentifier = async (
|
|||
|
||||
const verifySocialIdentifier = async (
|
||||
identifier: SocialConnectorPayload,
|
||||
ctx: WithLogContext,
|
||||
ctx: WithInteractionSieContext<WithLogContext>,
|
||||
tenant: TenantContext
|
||||
): Promise<SocialIdentifier> => {
|
||||
const userInfo = await verifySocialIdentity(identifier, ctx, tenant);
|
||||
|
@ -94,7 +95,10 @@ const verifySocialIdentifier = async (
|
|||
const {
|
||||
libraries: { ssoConnectors },
|
||||
} = tenant;
|
||||
await verifySsoOnlyEmailIdentifier(ssoConnectors, userInfo);
|
||||
|
||||
const { signInExperience } = ctx;
|
||||
|
||||
await verifySsoOnlyEmailIdentifier(ssoConnectors, userInfo, signInExperience);
|
||||
|
||||
return { key: 'social', connectorId: identifier.connectorId, userInfo };
|
||||
};
|
||||
|
@ -158,7 +162,7 @@ const verifySocialVerifiedIdentifier = async (
|
|||
* or phone, then the payload is valid.
|
||||
*/
|
||||
async function identifierPayloadVerification(
|
||||
ctx: WithLogContext,
|
||||
ctx: WithInteractionSieContext<WithLogContext>,
|
||||
tenant: TenantContext,
|
||||
identifierPayload: IdentifierPayload,
|
||||
interactionStorage: AnonymousInteractionResult
|
||||
|
|
|
@ -13,7 +13,7 @@ import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
|||
import type { IdentifierVerifiedInteractionResult } from '../types/index.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
|
||||
const { mockEsm } = createMockUtils(jest);
|
||||
|
||||
const findUserById = jest.fn();
|
||||
const hasUserWithEmail = jest.fn();
|
||||
|
|
|
@ -6,8 +6,6 @@ import {
|
|||
sendVerificationCode,
|
||||
} from '#src/api/interaction.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 {
|
||||
clearConnectorsByTypes,
|
||||
|
@ -16,17 +14,9 @@ import {
|
|||
setSmsConnector,
|
||||
} from '#src/helpers/connector.js';
|
||||
import { expectRejects, readVerificationCode } from '#src/helpers/index.js';
|
||||
import {
|
||||
enableAllPasswordSignInMethods,
|
||||
enableAllVerificationCodeSignInMethods,
|
||||
} from '#src/helpers/sign-in-experience.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUserProfile } from '#src/helpers/user.js';
|
||||
import {
|
||||
generateEmail,
|
||||
generatePassword,
|
||||
generateSsoConnectorName,
|
||||
generateUsername,
|
||||
} from '#src/utils.js';
|
||||
import { generatePassword, generateUsername } from '#src/utils.js';
|
||||
|
||||
describe('Register with identifiers sad path', () => {
|
||||
beforeAll(async () => {
|
||||
|
@ -58,40 +48,6 @@ describe('Register with identifiers sad path', () => {
|
|||
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', () => {
|
||||
beforeAll(async () => {
|
||||
// This function call will disable all sign-up settings by default
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,15 +1,13 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
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 {
|
||||
patchInteractionIdentifiers,
|
||||
putInteraction,
|
||||
sendVerificationCode,
|
||||
} from '#src/api/interaction.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 {
|
||||
clearConnectorsByTypes,
|
||||
|
@ -20,12 +18,7 @@ import {
|
|||
import { expectRejects, readVerificationCode } from '#src/helpers/index.js';
|
||||
import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser } from '#src/helpers/user.js';
|
||||
import {
|
||||
generateEmail,
|
||||
generatePassword,
|
||||
generatePhone,
|
||||
generateSsoConnectorName,
|
||||
} from '#src/utils.js';
|
||||
import { generateEmail, generatePhone } from '#src/utils.js';
|
||||
|
||||
describe('Sign-in flow sad path using verification-code identifiers', () => {
|
||||
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 () => {
|
||||
const notExistUserPhone = generatePhone();
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -7,8 +7,6 @@ import {
|
|||
putInteractionProfile,
|
||||
deleteUser,
|
||||
} 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 {
|
||||
clearConnectorsByTypes,
|
||||
|
@ -21,7 +19,6 @@ import {
|
|||
enableAllVerificationCodeSignInMethods,
|
||||
} from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js';
|
||||
import { generateSsoConnectorName } from '#src/utils.js';
|
||||
|
||||
describe('Sign-in flow using password identifiers', () => {
|
||||
beforeAll(async () => {
|
||||
|
@ -75,32 +72,6 @@ describe('Sign-in flow using password identifiers', () => {
|
|||
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 () => {
|
||||
const { userProfile, user } = await generateNewUser({ primaryPhone: true, password: true });
|
||||
const client = await initClient();
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
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 { 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 { clearSsoConnectors } from '#src/helpers/connector.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser } from '#src/helpers/user.js';
|
||||
import {
|
||||
generateEmail,
|
||||
generateName,
|
||||
generatePassword,
|
||||
generateSsoConnectorName,
|
||||
} from '#src/utils.js';
|
||||
import { generateName, generatePassword } from '#src/utils.js';
|
||||
|
||||
describe('Sign-in flow sad path using password identifiers', () => {
|
||||
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 () => {
|
||||
const {
|
||||
user,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue