mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
chore: legacy password policy cleanup
This commit is contained in:
parent
d10a23dba4
commit
803371d692
9 changed files with 10 additions and 168 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<T extends AuthedMeRouter>(
|
|||
|
||||
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;
|
||||
|
|
|
@ -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<User> => ({
|
||||
...mockUser,
|
||||
...user,
|
||||
...removeUndefinedKeys(user), // No undefined values will be returned from database
|
||||
})
|
||||
),
|
||||
} satisfies Partial<Libraries['users']>;
|
||||
|
@ -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 () => {
|
||||
|
|
|
@ -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<T extends AuthedRouter>(
|
|||
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<T extends AuthedRouter>(
|
|||
'/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],
|
||||
}),
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}$`
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in a new issue