mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
feat(core): guard sign up methods (#2194)
This commit is contained in:
parent
e936ad04ae
commit
55eb5f038d
12 changed files with 123 additions and 7 deletions
packages
core/src/routes/session
integration-tests
phrases/src/locales
|
@ -1,7 +1,7 @@
|
|||
import { User } from '@logto/schemas';
|
||||
import { SignUpIdentifier, User } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockUser } from '@/__mocks__';
|
||||
import { mockSignInExperience, mockUser } from '@/__mocks__';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
|
@ -10,6 +10,13 @@ import passwordlessRoutes, { registerRoute, signInRoute } from './passwordless';
|
|||
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
const findDefaultSignInExperience = jest.fn(async () => ({
|
||||
...mockSignInExperience,
|
||||
signUp: {
|
||||
...mockSignInExperience.signUp,
|
||||
identifier: SignUpIdentifier.EmailOrPhone,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
generateUserId: () => 'user1',
|
||||
|
@ -26,6 +33,10 @@ jest.mock('@/queries/user', () => ({
|
|||
hasUserWithEmail: async (email: string) => email === 'a@a.com',
|
||||
}));
|
||||
|
||||
jest.mock('@/queries/sign-in-experience', () => ({
|
||||
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
|
||||
}));
|
||||
|
||||
const sendPasscode = jest.fn(async () => ({ dbEntry: { id: 'connectorIdValue' } }));
|
||||
jest.mock('@/lib/passcode', () => ({
|
||||
createPasscode: async () => ({ id: 'id' }),
|
||||
|
@ -245,6 +256,21 @@ describe('session -> passwordlessRoutes', () => {
|
|||
.send({ phone: '13000000001', code: '1231' });
|
||||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
|
||||
it('throws if sign up identifier does not contain phone', async () => {
|
||||
findDefaultSignInExperience.mockResolvedValueOnce({
|
||||
...mockSignInExperience,
|
||||
signUp: {
|
||||
...mockSignInExperience.signUp,
|
||||
identifier: SignUpIdentifier.Email,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await sessionRequest
|
||||
.post(`${registerRoute}/sms/verify-passcode`)
|
||||
.send({ phone: '13000000001', code: '1234' });
|
||||
expect(response.statusCode).toEqual(422);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/register/passwordless/email/send-passcode', () => {
|
||||
|
@ -315,5 +341,20 @@ describe('session -> passwordlessRoutes', () => {
|
|||
.send({ email: 'a@a.com', code: '1231' });
|
||||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
|
||||
it('throws if sign up identifier does not contain email', async () => {
|
||||
findDefaultSignInExperience.mockResolvedValueOnce({
|
||||
...mockSignInExperience,
|
||||
signUp: {
|
||||
...mockSignInExperience.signUp,
|
||||
identifier: SignUpIdentifier.Phone,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await sessionRequest
|
||||
.post(`${registerRoute}/email/verify-passcode`)
|
||||
.send({ email: 'b@a.com', code: '1234' });
|
||||
expect(response.statusCode).toEqual(422);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { emailRegEx, phoneRegEx } from '@logto/core-kit';
|
||||
import { PasscodeType } from '@logto/schemas';
|
||||
import { PasscodeType, SignUpIdentifier } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode';
|
|||
import { assignInteractionResults } from '@/lib/session';
|
||||
import { generateUserId, insertUser } from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
|
||||
import {
|
||||
updateUserById,
|
||||
hasUserWithEmail,
|
||||
|
@ -155,6 +156,16 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
const type = 'RegisterSms';
|
||||
ctx.log(type, { phone, code });
|
||||
|
||||
const signInExperience = await findDefaultSignInExperience();
|
||||
assertThat(
|
||||
signInExperience.signUp.identifier === SignUpIdentifier.Phone ||
|
||||
signInExperience.signUp.identifier === SignUpIdentifier.EmailOrPhone,
|
||||
new RequestError({
|
||||
code: 'user.sign_up_method_not_enabled',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
|
||||
assertThat(
|
||||
!(await hasUserWithPhone(phone)),
|
||||
new RequestError({ code: 'user.phone_exists_register', status: 422 })
|
||||
|
@ -203,6 +214,16 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
const type = 'RegisterEmail';
|
||||
ctx.log(type, { email, code });
|
||||
|
||||
const signInExperience = await findDefaultSignInExperience();
|
||||
assertThat(
|
||||
signInExperience.signUp.identifier === SignUpIdentifier.Email ||
|
||||
signInExperience.signUp.identifier === SignUpIdentifier.EmailOrPhone,
|
||||
new RequestError({
|
||||
code: 'user.sign_up_method_not_enabled',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
|
||||
assertThat(
|
||||
!(await hasUserWithEmail(email)),
|
||||
new RequestError({ code: 'user.email_exists_register', status: 422 })
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { User, UserRole } from '@logto/schemas';
|
||||
import { SignUpIdentifier, User, UserRole } from '@logto/schemas';
|
||||
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockUser } from '@/__mocks__';
|
||||
import { mockSignInExperience, mockUser } from '@/__mocks__';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
|
@ -12,6 +12,13 @@ const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
|||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
const hasActiveUsers = jest.fn(async () => true);
|
||||
const findDefaultSignInExperience = jest.fn(async () => ({
|
||||
...mockSignInExperience,
|
||||
signUp: {
|
||||
...mockSignInExperience.signUp,
|
||||
identifier: SignUpIdentifier.Username,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
findUserById: async () => findUserById(),
|
||||
|
@ -27,6 +34,10 @@ jest.mock('@/queries/user', () => ({
|
|||
hasActiveUsers: async () => hasActiveUsers(),
|
||||
}));
|
||||
|
||||
jest.mock('@/queries/sign-in-experience', () => ({
|
||||
findDefaultSignInExperience: async () => findDefaultSignInExperience(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
async findUserByUsernameAndPassword(username: string, password: string) {
|
||||
if (username !== 'username' && username !== 'admin') {
|
||||
|
@ -209,5 +220,22 @@ describe('sessionRoutes', () => {
|
|||
.send({ username: 'username1', password: 'password' });
|
||||
expect(response.statusCode).toEqual(422);
|
||||
});
|
||||
|
||||
it('throws if sign up identifier is not username', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({ params: {} });
|
||||
|
||||
findDefaultSignInExperience.mockResolvedValueOnce({
|
||||
...mockSignInExperience,
|
||||
signUp: {
|
||||
...mockSignInExperience.signUp,
|
||||
identifier: SignUpIdentifier.Email,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await sessionRequest
|
||||
.post(registerRoute)
|
||||
.send({ username: 'username', password: 'password' });
|
||||
expect(response.statusCode).toEqual(422);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { passwordRegEx, usernameRegEx } from '@logto/core-kit';
|
||||
import { UserRole } from '@logto/schemas';
|
||||
import { SignUpIdentifier, UserRole } from '@logto/schemas';
|
||||
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { object, string } from 'zod';
|
||||
|
@ -13,6 +13,7 @@ import {
|
|||
insertUser,
|
||||
} from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
|
||||
import { hasUser, hasActiveUsers, updateUserById } from '@/queries/user';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
|
@ -65,6 +66,15 @@ export default function usernamePasswordRoutes<T extends AnonymousRouter>(
|
|||
const type = 'RegisterUsernamePassword';
|
||||
ctx.log(type, { username });
|
||||
|
||||
const signInExperience = await findDefaultSignInExperience();
|
||||
assertThat(
|
||||
signInExperience.signUp.identifier === SignUpIdentifier.Username,
|
||||
new RequestError({
|
||||
code: 'user.sign_up_method_not_enabled',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
|
||||
assertThat(
|
||||
!(await hasUser(username)),
|
||||
new RequestError({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { User } from '@logto/schemas';
|
||||
import { SignUpIdentifier, User } from '@logto/schemas';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import { HTTPError } from 'got';
|
||||
|
||||
|
@ -14,6 +14,7 @@ import {
|
|||
bindWithSocial,
|
||||
getAuthWithSocial,
|
||||
signInWithSocial,
|
||||
updateSignInExperience,
|
||||
} from '@/api';
|
||||
import MockClient from '@/client';
|
||||
import { generateUsername, generatePassword } from '@/utils';
|
||||
|
@ -71,6 +72,10 @@ export const setUpConnector = async (connectorId: string, config: Record<string,
|
|||
assert(connector.enabled, new Error('Connector Setup Failed'));
|
||||
};
|
||||
|
||||
export const setSignUpIdentifier = async (identifier: SignUpIdentifier) => {
|
||||
await updateSignInExperience({ signUp: { identifier, password: true, verify: true } });
|
||||
};
|
||||
|
||||
type PasscodeRecord = {
|
||||
phone?: string;
|
||||
address?: string;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SignUpIdentifier } from '@logto/schemas';
|
||||
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
setUpConnector,
|
||||
readPasscode,
|
||||
createUserByAdmin,
|
||||
setSignUpIdentifier,
|
||||
} from '@/helpers';
|
||||
import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils';
|
||||
|
||||
|
@ -45,6 +47,7 @@ describe('username and password flow', () => {
|
|||
describe('email passwordless flow', () => {
|
||||
beforeAll(async () => {
|
||||
await setUpConnector(mockEmailConnectorId, mockEmailConnectorConfig);
|
||||
await setSignUpIdentifier(SignUpIdentifier.Email);
|
||||
});
|
||||
|
||||
// Since we can not create a email register user throw admin. Have to run the register then sign-in concurrently.
|
||||
|
@ -118,6 +121,7 @@ describe('email passwordless flow', () => {
|
|||
describe('sms passwordless flow', () => {
|
||||
beforeAll(async () => {
|
||||
await setUpConnector(mockSmsConnectorId, mockSmsConnectorConfig);
|
||||
await setSignUpIdentifier(SignUpIdentifier.Phone);
|
||||
});
|
||||
|
||||
// Since we can not create a sms register user throw admin. Have to run the register then sign-in concurrently.
|
||||
|
@ -194,6 +198,7 @@ describe('sign-in and sign-out', () => {
|
|||
|
||||
beforeAll(async () => {
|
||||
await createUserByAdmin(username, password);
|
||||
await setSignUpIdentifier(SignUpIdentifier.Username);
|
||||
});
|
||||
|
||||
it('verify sign-in and then sign-out', async () => {
|
||||
|
|
|
@ -43,6 +43,7 @@ const errors = {
|
|||
invalid_role_names: 'role names ({{roleNames}}) are not valid',
|
||||
cannot_delete_self: 'You cannot delete yourself.',
|
||||
same_password: 'Your new password can not be the same as current password.',
|
||||
sign_up_method_not_enabled: 'This sign up method is not enabled.',
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
|
||||
|
|
|
@ -44,6 +44,7 @@ const errors = {
|
|||
invalid_role_names: 'les noms de rôles ({{roleNames}}) ne sont pas valides',
|
||||
cannot_delete_self: 'You cannot delete yourself.', // UNTRANSLATED
|
||||
same_password: 'Your new password can not be the same as current password.', // UNTRANSLATED
|
||||
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.",
|
||||
|
|
|
@ -42,6 +42,7 @@ const errors = {
|
|||
invalid_role_names: '직책 명({{roleNames}})이 유효하지 않아요.',
|
||||
cannot_delete_self: 'You cannot delete yourself.', // UNTRANSLATED
|
||||
same_password: 'Your new password can not be the same as current password.', // UNTRANSLATED
|
||||
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.',
|
||||
|
|
|
@ -42,6 +42,7 @@ const errors = {
|
|||
invalid_role_names: '({{roleNames}}) não são válidos',
|
||||
cannot_delete_self: 'Não se pode remover a si mesmo.',
|
||||
same_password: 'Your new password can not be the same as current password.', // UNTRANSLATED
|
||||
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: 'O método de enncriptação {{name}} não é suportado.',
|
||||
|
|
|
@ -43,6 +43,7 @@ const errors = {
|
|||
invalid_role_names: '({{roleNames}}) rol adları geçerli değil.',
|
||||
cannot_delete_self: 'You cannot delete yourself.', // UNTRANSLATED
|
||||
same_password: 'Your new password can not be the same as current password.', // UNTRANSLATED
|
||||
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.',
|
||||
|
|
|
@ -42,6 +42,7 @@ const errors = {
|
|||
invalid_role_names: '角色名称({{roleNames}})无效',
|
||||
cannot_delete_self: '你无法删除自己',
|
||||
same_password: '新设置的密码不可与当前密码相同',
|
||||
sign_up_method_not_enabled: '注册方式尚未启用', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '不支持的加密方法 {{name}}',
|
||||
|
|
Loading…
Add table
Reference in a new issue