diff --git a/packages/console/src/pages/Profile/containers/ChangePasswordModal/index.tsx b/packages/console/src/pages/Profile/containers/ChangePasswordModal/index.tsx index db800294e..bb7b6d52f 100644 --- a/packages/console/src/pages/Profile/containers/ChangePasswordModal/index.tsx +++ b/packages/console/src/pages/Profile/containers/ChangePasswordModal/index.tsx @@ -1,4 +1,3 @@ -import { passwordRegEx } from '@logto/core-kit'; import type { KeyboardEventHandler } from 'react'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; @@ -94,14 +93,6 @@ function ChangePasswordModal() { placeholder={t('profile.password.password')} {...register('newPassword', { required: t('profile.password.required'), - minLength: { - value: 8, - message: t('profile.password.min_length', { min: 8 }), - }, - pattern: { - value: passwordRegEx, - message: t('errors.password_pattern_error'), - }, })} // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus diff --git a/packages/console/src/utils/password.ts b/packages/console/src/utils/password.ts index d6f70c463..e61cbb4c1 100644 --- a/packages/console/src/utils/password.ts +++ b/packages/console/src/utils/password.ts @@ -1,14 +1,3 @@ -import { passwordRegEx } from '@logto/core-kit'; import { nanoid } from 'nanoid'; -// Note: password requires a minimum of 8 characters and contains a mix of letters, numbers, and symbols. -export const generateRandomPassword = (length = 8) => { - // eslint-disable-next-line @silverhand/fp/no-let - let generated = nanoid(length); - while (!passwordRegEx.test(generated)) { - // eslint-disable-next-line @silverhand/fp/no-mutation - generated = nanoid(length); - } - - return generated; -}; +export const generateRandomPassword = () => nanoid(8); diff --git a/packages/core/src/routes-me/user.ts b/packages/core/src/routes-me/user.ts index ac5cc4a8e..90fe834cc 100644 --- a/packages/core/src/routes-me/user.ts +++ b/packages/core/src/routes-me/user.ts @@ -1,4 +1,4 @@ -import { emailRegEx, passwordRegEx, usernameRegEx } from '@logto/core-kit'; +import { emailRegEx, usernameRegEx } from '@logto/core-kit'; import { userInfoSelectFields, jsonObjectGuard } from '@logto/schemas'; import { conditional, pick } from '@silverhand/essentials'; import { literal, object, string } from 'zod'; @@ -123,7 +123,7 @@ export default function userRoutes( router.post( '/password', - koaGuard({ body: object({ password: string().regex(passwordRegEx) }) }), + koaGuard({ body: object({ password: string().min(1) }) }), async (ctx, next) => { const { id: userId } = ctx.auth; const { password } = ctx.guard.body; diff --git a/packages/core/src/routes/admin-user.test.ts b/packages/core/src/routes/admin-user.test.ts index 88c82a1d8..f9d816845 100644 --- a/packages/core/src/routes/admin-user.test.ts +++ b/packages/core/src/routes/admin-user.test.ts @@ -1,6 +1,7 @@ import type { CreateUser, Role, SignInExperience, User } from '@logto/schemas'; import { RoleType } from '@logto/schemas'; import { createMockUtils, pickDefault } from '@logto/shared/esm'; +import { removeUndefinedKeys } from '@silverhand/essentials'; import { mockUser, mockUserResponse } from '#src/__mocks__/index.js'; import RequestError from '#src/errors/RequestError/index.js'; @@ -84,7 +85,7 @@ const usersLibraries = { insertUser: jest.fn( async (user: CreateUser): Promise => ({ ...mockUser, - ...user, + ...removeUndefinedKeys(user), // No undefined values will be returned from database }) ), } satisfies Partial; @@ -125,14 +126,14 @@ describe('adminUserRoutes', () => { }); }); - it('POST /users should throw with invalid input params', async () => { + it('POST /users should be ok with simple passwords', async () => { const username = 'MJAtLogto'; const name = 'Michael'; // Invalid input format await expect( userRequest.post('/users').send({ username, password: 'abc', name }) - ).resolves.toHaveProperty('status', 400); + ).resolves.toHaveProperty('status', 200); }); it('POST /users should throw if username exists', async () => { diff --git a/packages/core/src/routes/admin-user.ts b/packages/core/src/routes/admin-user.ts index a0d08bd3f..ab6c2b3e8 100644 --- a/packages/core/src/routes/admin-user.ts +++ b/packages/core/src/routes/admin-user.ts @@ -1,4 +1,4 @@ -import { emailRegEx, passwordRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit'; +import { emailRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit'; import { jsonObjectGuard, userInfoSelectFields, userProfileResponseGuard } from '@logto/schemas'; import { conditional, pick } from '@silverhand/essentials'; import { boolean, literal, object, string } from 'zod'; @@ -100,7 +100,7 @@ export default function adminUserRoutes( primaryPhone: string().regex(phoneRegEx), primaryEmail: string().regex(emailRegEx), username: string().regex(usernameRegEx), - password: string().regex(passwordRegEx), + password: string().min(1), name: string(), }).partial(), response: userProfileResponseGuard, @@ -183,7 +183,7 @@ export default function adminUserRoutes( '/users/:userId/password', koaGuard({ params: object({ userId: string() }), - body: object({ password: string().regex(passwordRegEx) }), + body: object({ password: string().min(1) }), response: userProfileResponseGuard, status: [200, 422], }), diff --git a/packages/toolkit/core-kit/src/regex.test.ts b/packages/toolkit/core-kit/src/regex.test.ts deleted file mode 100644 index c959c5bad..000000000 --- a/packages/toolkit/core-kit/src/regex.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { passwordRegEx } from './regex.js'; - -describe('passwordRegEx', () => { - it('should match password with at least 8 chars', () => { - expect(passwordRegEx.test('1234ddf')).toBeFalsy(); - expect(passwordRegEx.test('1234ddf!')).toBeTruthy(); - }); - - it('password should not contains non ASCII visible chars', () => { - expect(passwordRegEx.test('a1?aaaaa测试')).toBeFalsy(); - - expect(passwordRegEx.test('a1?aaaaa测试')).toBeFalsy(); - - expect(passwordRegEx.test('a1?aaaaa🌹')).toBeFalsy(); - - expect(passwordRegEx.test('a1?aaaaa')).toBeTruthy(); - }); - - describe('password should contains at least 2 of 3 types of chars', () => { - const singleTypeChars = ['aaaaaaaa', '11111111', '!@#$%^&*(())']; - - it.each(singleTypeChars)('single typed password format %p should be invalid', (password) => { - expect(passwordRegEx.test(password)).toBeFalsy(); - }); - - const doubleTypeChars = [ - 'asdfghj1', - 'asdfghj$', - '1234567@', - '1234567a', - '!@#$%^&1', - '!@#$%^&a', - ]; - - it.each(doubleTypeChars)('double typed password format %p should be valid', (password) => { - expect(passwordRegEx.test(password)).toBeTruthy(); - }); - - const tripleTypeChars = ['ASD!@#45', 'a!@#$%123', '1ASDfg654', '*123345GHJ']; - - it.each(tripleTypeChars)('triple typed password format %p should be valid', (password) => { - expect(passwordRegEx.test(password)).toBeTruthy(); - }); - }); -}); diff --git a/packages/toolkit/core-kit/src/regex.ts b/packages/toolkit/core-kit/src/regex.ts index 3b010ad08..f7a2a2700 100644 --- a/packages/toolkit/core-kit/src/regex.ts +++ b/packages/toolkit/core-kit/src/regex.ts @@ -7,12 +7,3 @@ export const mobileUriSchemeProtocolRegEx = /^[a-z][\d+_a-z-]*(\.[\d+_a-z-]+)+:$ export const hexColorRegEx = /^#[\da-f]{3}([\da-f]{3})?$/i; export const dateRegex = /^\d{4}(-\d{2}){2}/; export const noSpaceRegEx = /^\S+$/; - -const atLeastOneDigitAndOneLetters = /(?=.*\d)(?=.*[A-Za-z])/; -const atLeastOneDigitAndOneSpecialChar = /(?=.*\d)(?=.*[!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/; -const atLeastOneLetterAndOneSpecialChar = /(?=.*[A-Za-z])(?=.*[!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/; -const allowedChars = /[\w!"#$%&'()*+,./:;<=>?@[\]^`{|}~-]{8,}/; - -export const passwordRegEx = new RegExp( - `^(${atLeastOneDigitAndOneLetters.source}|${atLeastOneDigitAndOneSpecialChar.source}|${atLeastOneLetterAndOneSpecialChar.source})${allowedChars.source}$` -); diff --git a/packages/ui/src/utils/form.test.ts b/packages/ui/src/utils/form.test.ts deleted file mode 100644 index 6ec4e8fe7..000000000 --- a/packages/ui/src/utils/form.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { validatePassword } from './form'; - -describe('password format', () => { - it('password min length should be 8', () => { - expect(validatePassword('a1?')).toEqual({ code: 'password_min_length', data: { min: 8 } }); - expect(validatePassword('aaa123aa')).toBe(undefined); - }); - - it('password should not contains non ASCII visible chars', () => { - expect(validatePassword('a1?aaaaa测试')).toEqual({ - code: 'invalid_password', - data: { min: 8 }, - }); - - expect(validatePassword('a1?aaaaa测试')).toEqual({ - code: 'invalid_password', - data: { min: 8 }, - }); - - expect(validatePassword('a1?aaaaa🌹')).toEqual({ - code: 'invalid_password', - data: { min: 8 }, - }); - - expect(validatePassword('a1?aaaaa')).toBe(undefined); - }); - - describe('password should contains at least 2 of 3 types of chars', () => { - const singleTypeChars = ['aaaaaaaa', '11111111', '!@#$%^&*(())']; - - it.each(singleTypeChars)('single typed password format %p should be invalid', (password) => { - expect(validatePassword(password)).toEqual({ - code: 'invalid_password', - data: { min: 8 }, - }); - }); - - const doubleTypeChars = [ - 'asdfghj1', - 'asdfghj$', - '1234567@', - '1234567a', - '!@#$%^&1', - '!@#$%^&a', - ]; - - it.each(doubleTypeChars)('double typed password format %p should be valid', (password) => { - expect(validatePassword(password)).toBe(undefined); - }); - - const tripleTypeChars = ['ASD!@#45', 'a!@#$%123', '1ASDfg654', '*123345GHJ']; - - it.each(tripleTypeChars)('triple typed password format %p should be valid', (password) => { - expect(validatePassword(password)).toBe(undefined); - }); - }); -}); diff --git a/packages/ui/src/utils/form.ts b/packages/ui/src/utils/form.ts index 66a74ffe7..36f47f8d2 100644 --- a/packages/ui/src/utils/form.ts +++ b/packages/ui/src/utils/form.ts @@ -10,15 +10,6 @@ import { parseE164Number, parsePhoneNumber } from '@/utils/country-code'; const { t } = i18next; -// We validate the password format step by step to provide more detailed error messages at the front end. -// The overall password format regex passwordRegEx is defined in the '@logto/core-kit' need to align these two. -const specialChars = /[!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-]/; -const digits = /\d/; -const letters = /[A-Za-z]/; -const allowedChars = /^[\w!"#$%&'()*+,./:;<=>?@[\]^`{|}~-]*$/; - -export const passwordMinLength = 8; - export const validateUsername = (username: string): ErrorType | undefined => { if (!username) { return 'username_required'; @@ -55,25 +46,6 @@ export const validatePhone = (value: string): ErrorType | undefined => { } }; -export const validatePassword = (value: string): ErrorType | undefined => { - const hasDigits = digits.test(value); - const hasLetters = letters.test(value); - const hasSpecialChars = specialChars.test(value); - const nonInvalidChars = allowedChars.test(value); - - if (!nonInvalidChars) { - return { code: 'invalid_password', data: { min: passwordMinLength } }; - } - - if (value.length < passwordMinLength) { - return { code: 'password_min_length', data: { min: passwordMinLength } }; - } - - if ((hasDigits ? 1 : 0) + (hasLetters ? 1 : 0) + (hasSpecialChars ? 1 : 0) < 2) { - return { code: 'invalid_password', data: { min: passwordMinLength } }; - } -}; - export const validateIdentifierField = (type: IdentifierInputType, value: string) => { switch (type) { case SignInIdentifier.Username: {