0
Fork 0
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:
Gao Sun 2023-09-12 16:12:39 +08:00
parent d10a23dba4
commit 803371d692
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
9 changed files with 10 additions and 168 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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