0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

feat(core): guard sign up methods ()

This commit is contained in:
wangsijie 2022-10-20 10:57:49 +08:00 committed by GitHub
parent e936ad04ae
commit 55eb5f038d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 123 additions and 7 deletions

View file

@ -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);
});
});
});

View file

@ -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 })

View file

@ -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);
});
});
});

View file

@ -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({

View file

@ -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;

View file

@ -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 () => {

View file

@ -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.',

View file

@ -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.",

View file

@ -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}} 암호화 방법을 지원하지 않아요.',

View file

@ -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.',

View file

@ -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.',

View file

@ -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}}',