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) {
|
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(
|
||||||
|
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 { 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();
|
||||||
|
|
||||||
|
|
|
@ -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,
|
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();
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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