Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core,schemas): add secondary sign-up identifiers #7127

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/purple-waves-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@logto/core": patch
---

remove multiple sign-in experience settings restrictions

For better customization flexibility, we have removed following restrictions in the sign-in experience "sign-in and sign-up" settings:

1. The `password` field in sign-up settings is no longer required when username is set as the sign-up identifier. Developers may request a username without requiring a password during the sign-up process.

Note: If username is the only sign-up identifier, users without a password will not be able to sign in. Developers or administrators should carefully configure the sign-up and sign-in settings to ensure a smooth user experience.

Users can still set password via [account API](https://docs.logto.io/end-user-flows/account-settings/by-account-api) after sign-up.

2. The requirement that all sign-up identifiers must also be enabled as sign-in identifiers has been removed.
98 changes: 0 additions & 98 deletions packages/core/src/libraries/sign-in-experience/sign-in.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,104 +113,6 @@ describe('validate sign-in', () => {
});
});

describe('The sign up identifier must be included in sign in', () => {
it('throws when sign up is username and sign in methods does not include username', () => {
expect(() => {
validateSignIn(
{
methods: [
{
...mockSignInMethod,
identifier: SignInIdentifier.Phone,
},
],
},
{
...mockSignUp,
identifiers: [SignInIdentifier.Username],
},
enabledConnectors
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
});

it('throws when sign up is email and sign in methods does not include email', () => {
expect(() => {
validateSignIn(
{
methods: [
{
...mockSignInMethod,
identifier: SignInIdentifier.Username,
},
],
},
{
...mockSignUp,
identifiers: [SignInIdentifier.Email],
},
enabledConnectors
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
});

it('throws when sign up is phone and sign in methods does not include phone', () => {
expect(() => {
validateSignIn(
{
methods: [
{
...mockSignInMethod,
identifier: SignInIdentifier.Username,
},
],
},
{
...mockSignUp,
identifiers: [SignInIdentifier.Phone],
},
enabledConnectors
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
});

it('throws when sign up is `email or phone` and sign in methods does not include email and phone', () => {
expect(() => {
validateSignIn(
{
methods: [
{
...mockSignInMethod,
identifier: SignInIdentifier.Email,
},
],
},
{
...mockSignUp,
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
},
enabledConnectors
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
});
});

it('throws when sign up requires set a password and sign in password is not enabled', () => {
expect(() => {
validateSignIn(
Expand Down
29 changes: 0 additions & 29 deletions packages/core/src/libraries/sign-in-experience/sign-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,35 +47,6 @@ export const validateSignIn = (
})
);

for (const identifier of signUp.identifiers) {
if (identifier === SignInIdentifier.Username) {
assertThat(
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Username),
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
}

if (identifier === SignInIdentifier.Email) {
assertThat(
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Email),
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
}

if (identifier === SignInIdentifier.Phone) {
assertThat(
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Phone),
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
}
}

if (signUp.password) {
assertThat(
signIn.methods.every(({ password }) => password),
Expand Down
152 changes: 134 additions & 18 deletions packages/core/src/libraries/sign-in-experience/sign-up.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,70 @@
import { ConnectorType, SignInIdentifier } from '@logto/schemas';
import { createMockUtils } from '@logto/shared/esm';
import {
AlternativeSignUpIdentifier,
ConnectorType,
SignInIdentifier,
type SignUp,
} from '@logto/schemas';

import { mockAliyunDmConnector, mockAliyunSmsConnector, mockSignUp } from '#src/__mocks__/index.js';
import RequestError from '#src/errors/RequestError/index.js';

const { jest } = import.meta;
const { mockEsmWithActual } = createMockUtils(jest);

const enabledConnectors = [mockAliyunDmConnector, mockAliyunSmsConnector];

const { validateSignUp } = await import('./sign-up.js');

describe('validate sign-up', () => {
it('should throw when setting secondary identifiers without primary identifiers', async () => {
expect(() => {
validateSignUp(
{
...mockSignUp,
identifiers: [],
secondaryIdentifiers: [{ identifier: SignInIdentifier.Email }],
},
enabledConnectors
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.missing_sign_up_identifiers',
})
);
});

describe('Sign up identifiers must be unique.', () => {
const errorTestCase: Array<Pick<SignUp, 'identifiers' | 'secondaryIdentifiers'>> = [
{ identifiers: [SignInIdentifier.Email, SignInIdentifier.Email] },
{
identifiers: [SignInIdentifier.Username],
secondaryIdentifiers: [{ identifier: SignInIdentifier.Username }],
},
{
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
secondaryIdentifiers: [{ identifier: SignInIdentifier.Phone }],
},
{
identifiers: [SignInIdentifier.Phone],
secondaryIdentifiers: [{ identifier: AlternativeSignUpIdentifier.EmailOrPhone }],
},
];

test.each(errorTestCase)(
'should throw when there are duplicated sign up identifiers',
async (signUp) => {
expect(() => {
validateSignUp({ ...mockSignUp, ...signUp }, enabledConnectors);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.duplicated_sign_up_identifiers',
})
);
}
);
});

describe('There must be at least one connector for the specific identifier.', () => {
test('should throw when there is no email connector and identifier is email', async () => {
expect(() => {
validateSignUp({ ...mockSignUp, identifiers: [SignInIdentifier.Email] }, []);
validateSignUp({ ...mockSignUp, identifiers: [SignInIdentifier.Email], verify: true }, []);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
Expand All @@ -30,6 +79,7 @@ describe('validate sign-up', () => {
{
...mockSignUp,
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
verify: true,
},
[]
);
Expand All @@ -43,7 +93,7 @@ describe('validate sign-up', () => {

test('should throw when there is no sms connector and identifier is phone', async () => {
expect(() => {
validateSignUp({ ...mockSignUp, identifiers: [SignInIdentifier.Phone] }, []);
validateSignUp({ ...mockSignUp, identifiers: [SignInIdentifier.Phone], verify: true }, []);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
Expand All @@ -69,19 +119,59 @@ describe('validate sign-up', () => {
})
);
});
});

test('should throw when identifier is username and password is false', async () => {
expect(() => {
validateSignUp(
{ ...mockSignUp, identifiers: [SignInIdentifier.Username], password: false },
enabledConnectors
test('should throw when there is no email connector and secondary identifier is email', async () => {
expect(() => {
validateSignUp(
{
...mockSignUp,
secondaryIdentifiers: [{ identifier: SignInIdentifier.Email, verify: true }],
},
[]
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Email,
})
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.username_requires_password',
})
);
});

test('should throw when there is no sms connector and secondary identifier is phone', async () => {
expect(() => {
validateSignUp(
{
...mockSignUp,
secondaryIdentifiers: [{ identifier: SignInIdentifier.Phone, verify: true }],
},
[]
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Sms,
})
);
});

test('should throw when there is no email connector and secondary identifier is email or phone', async () => {
expect(() => {
validateSignUp(
{
...mockSignUp,
secondaryIdentifiers: [
{ identifier: AlternativeSignUpIdentifier.EmailOrPhone, verify: true },
],
},
[]
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Email,
})
);
});
});

describe('verify should be true for passwordless identifier', () => {
Expand Down Expand Up @@ -127,5 +217,31 @@ describe('validate sign-up', () => {
})
);
});

test.each([
{
identifier: SignInIdentifier.Email,
},
{
identifier: SignInIdentifier.Phone,
},
{
identifier: AlternativeSignUpIdentifier.EmailOrPhone,
},
])('should throw when identifier is %p and verify is not true', async (identifier) => {
expect(() => {
validateSignUp(
{
...mockSignUp,
secondaryIdentifiers: [identifier],
},
enabledConnectors
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.passwordless_requires_verify',
})
);
});
});
});
Loading
Loading