mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
🔐 feat: Add Configurable Min. Password Length (#9315)
- Added support for a minimum password length defined by the MIN_PASSWORD_LENGTH environment variable. - Updated login, registration, and reset password forms to utilize the configured minimum length. - Enhanced validation schemas to reflect the new minimum password length requirement. - Included tests to ensure the minimum password length functionality works as expected.
This commit is contained in:
parent
ea3b671182
commit
ba424666f8
8 changed files with 87 additions and 9 deletions
|
|
@ -40,6 +40,13 @@ NO_INDEX=true
|
||||||
# Defaulted to 1.
|
# Defaulted to 1.
|
||||||
TRUST_PROXY=1
|
TRUST_PROXY=1
|
||||||
|
|
||||||
|
# Minimum password length for user authentication
|
||||||
|
# Default: 8
|
||||||
|
# Note: When using LDAP authentication, you may want to set this to 1
|
||||||
|
# to bypass local password validation, as LDAP servers handle their own
|
||||||
|
# password policies.
|
||||||
|
# MIN_PASSWORD_LENGTH=8
|
||||||
|
|
||||||
#===============#
|
#===============#
|
||||||
# JSON Logging #
|
# JSON Logging #
|
||||||
#===============#
|
#===============#
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,11 @@ router.get('/', async function (req, res) {
|
||||||
openidReuseTokens,
|
openidReuseTokens,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const minPasswordLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10);
|
||||||
|
if (minPasswordLength && !isNaN(minPasswordLength)) {
|
||||||
|
payload.minPasswordLength = minPasswordLength;
|
||||||
|
}
|
||||||
|
|
||||||
payload.mcpServers = {};
|
payload.mcpServers = {};
|
||||||
const getMCPServers = () => {
|
const getMCPServers = () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
const { z } = require('zod');
|
const { z } = require('zod');
|
||||||
|
|
||||||
|
const MIN_PASSWORD_LENGTH = parseInt(process.env.MIN_PASSWORD_LENGTH, 10) || 8;
|
||||||
|
|
||||||
const allowedCharactersRegex = new RegExp(
|
const allowedCharactersRegex = new RegExp(
|
||||||
'^[' +
|
'^[' +
|
||||||
'a-zA-Z0-9_.@#$%&*()' + // Basic Latin characters and symbols
|
'a-zA-Z0-9_.@#$%&*()' + // Basic Latin characters and symbols
|
||||||
|
|
@ -32,7 +34,7 @@ const loginSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8)
|
.min(MIN_PASSWORD_LENGTH)
|
||||||
.max(128)
|
.max(128)
|
||||||
.refine((value) => value.trim().length > 0, {
|
.refine((value) => value.trim().length > 0, {
|
||||||
message: 'Password cannot be only spaces',
|
message: 'Password cannot be only spaces',
|
||||||
|
|
@ -50,14 +52,14 @@ const registerSchema = z
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8)
|
.min(MIN_PASSWORD_LENGTH)
|
||||||
.max(128)
|
.max(128)
|
||||||
.refine((value) => value.trim().length > 0, {
|
.refine((value) => value.trim().length > 0, {
|
||||||
message: 'Password cannot be only spaces',
|
message: 'Password cannot be only spaces',
|
||||||
}),
|
}),
|
||||||
confirm_password: z
|
confirm_password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8)
|
.min(MIN_PASSWORD_LENGTH)
|
||||||
.max(128)
|
.max(128)
|
||||||
.refine((value) => value.trim().length > 0, {
|
.refine((value) => value.trim().length > 0, {
|
||||||
message: 'Password cannot be only spaces',
|
message: 'Password cannot be only spaces',
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ describe('Zod Schemas', () => {
|
||||||
email: 'john@example.com',
|
email: 'john@example.com',
|
||||||
password: 'password123',
|
password: 'password123',
|
||||||
confirm_password: 'password123',
|
confirm_password: 'password123',
|
||||||
extraField: 'I shouldn\'t be here',
|
extraField: "I shouldn't be here",
|
||||||
});
|
});
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
@ -407,7 +407,7 @@ describe('Zod Schemas', () => {
|
||||||
'john{doe}', // Contains `{` and `}`
|
'john{doe}', // Contains `{` and `}`
|
||||||
'j', // Only one character
|
'j', // Only one character
|
||||||
'a'.repeat(81), // More than 80 characters
|
'a'.repeat(81), // More than 80 characters
|
||||||
'\' OR \'1\'=\'1\'; --', // SQL Injection
|
"' OR '1'='1'; --", // SQL Injection
|
||||||
'{$ne: null}', // MongoDB Injection
|
'{$ne: null}', // MongoDB Injection
|
||||||
'<script>alert("XSS")</script>', // Basic XSS
|
'<script>alert("XSS")</script>', // Basic XSS
|
||||||
'"><script>alert("XSS")</script>', // XSS breaking out of an attribute
|
'"><script>alert("XSS")</script>', // XSS breaking out of an attribute
|
||||||
|
|
@ -453,4 +453,64 @@ describe('Zod Schemas', () => {
|
||||||
expect(result).toBe('name: String must contain at least 3 character(s)');
|
expect(result).toBe('name: String must contain at least 3 character(s)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('MIN_PASSWORD_LENGTH environment variable', () => {
|
||||||
|
// Note: These tests verify the behavior based on whatever MIN_PASSWORD_LENGTH
|
||||||
|
// was set when the validators module was loaded
|
||||||
|
const minLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10) || 8;
|
||||||
|
|
||||||
|
it('should respect the configured minimum password length for login', () => {
|
||||||
|
// Test password exactly at minimum length
|
||||||
|
const resultValid = loginSchema.safeParse({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'a'.repeat(minLength),
|
||||||
|
});
|
||||||
|
expect(resultValid.success).toBe(true);
|
||||||
|
|
||||||
|
// Test password one character below minimum
|
||||||
|
if (minLength > 1) {
|
||||||
|
const resultInvalid = loginSchema.safeParse({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'a'.repeat(minLength - 1),
|
||||||
|
});
|
||||||
|
expect(resultInvalid.success).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect the configured minimum password length for registration', () => {
|
||||||
|
// Test password exactly at minimum length
|
||||||
|
const resultValid = registerSchema.safeParse({
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
password: 'a'.repeat(minLength),
|
||||||
|
confirm_password: 'a'.repeat(minLength),
|
||||||
|
});
|
||||||
|
expect(resultValid.success).toBe(true);
|
||||||
|
|
||||||
|
// Test password one character below minimum
|
||||||
|
if (minLength > 1) {
|
||||||
|
const resultInvalid = registerSchema.safeParse({
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
password: 'a'.repeat(minLength - 1),
|
||||||
|
confirm_password: 'a'.repeat(minLength - 1),
|
||||||
|
});
|
||||||
|
expect(resultInvalid.success).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge case of very short minimum password length', () => {
|
||||||
|
// This test is meaningful only if MIN_PASSWORD_LENGTH is set to a very low value
|
||||||
|
if (minLength <= 3) {
|
||||||
|
const result = loginSchema.safeParse({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: 'abc',
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(minLength <= 3);
|
||||||
|
} else {
|
||||||
|
// Skip this test if minimum length is > 3
|
||||||
|
expect(true).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,10 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||||
aria-label={localize('com_auth_password')}
|
aria-label={localize('com_auth_password')}
|
||||||
{...register('password', {
|
{...register('password', {
|
||||||
required: localize('com_auth_password_required'),
|
required: localize('com_auth_password_required'),
|
||||||
minLength: { value: 8, message: localize('com_auth_password_min_length') },
|
minLength: {
|
||||||
|
value: startupConfig?.minPasswordLength || 8,
|
||||||
|
message: localize('com_auth_password_min_length'),
|
||||||
|
},
|
||||||
maxLength: { value: 128, message: localize('com_auth_password_max_length') },
|
maxLength: { value: 128, message: localize('com_auth_password_max_length') },
|
||||||
})}
|
})}
|
||||||
aria-invalid={!!errors.password}
|
aria-invalid={!!errors.password}
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ const Registration: React.FC = () => {
|
||||||
{renderInput('password', 'com_auth_password', 'password', {
|
{renderInput('password', 'com_auth_password', 'password', {
|
||||||
required: localize('com_auth_password_required'),
|
required: localize('com_auth_password_required'),
|
||||||
minLength: {
|
minLength: {
|
||||||
value: 8,
|
value: startupConfig?.minPasswordLength || 8,
|
||||||
message: localize('com_auth_password_min_length'),
|
message: localize('com_auth_password_min_length'),
|
||||||
},
|
},
|
||||||
maxLength: {
|
maxLength: {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ function ResetPassword() {
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
const password = watch('password');
|
const password = watch('password');
|
||||||
const resetPassword = useResetPasswordMutation();
|
const resetPassword = useResetPasswordMutation();
|
||||||
const { setError, setHeaderText } = useOutletContext<TLoginLayoutContext>();
|
const { setError, setHeaderText, startupConfig } = useOutletContext<TLoginLayoutContext>();
|
||||||
|
|
||||||
const onSubmit = (data: TResetPassword) => {
|
const onSubmit = (data: TResetPassword) => {
|
||||||
resetPassword.mutate(data, {
|
resetPassword.mutate(data, {
|
||||||
|
|
@ -83,7 +83,7 @@ function ResetPassword() {
|
||||||
{...register('password', {
|
{...register('password', {
|
||||||
required: localize('com_auth_password_required'),
|
required: localize('com_auth_password_required'),
|
||||||
minLength: {
|
minLength: {
|
||||||
value: 8,
|
value: startupConfig?.minPasswordLength || 8,
|
||||||
message: localize('com_auth_password_min_length'),
|
message: localize('com_auth_password_min_length'),
|
||||||
},
|
},
|
||||||
maxLength: {
|
maxLength: {
|
||||||
|
|
|
||||||
|
|
@ -641,6 +641,7 @@ export type TStartupConfig = {
|
||||||
sharePointPickerGraphScope?: string;
|
sharePointPickerGraphScope?: string;
|
||||||
sharePointPickerSharePointScope?: string;
|
sharePointPickerSharePointScope?: string;
|
||||||
openidReuseTokens?: boolean;
|
openidReuseTokens?: boolean;
|
||||||
|
minPasswordLength?: number;
|
||||||
webSearch?: {
|
webSearch?: {
|
||||||
searchProvider?: SearchProviders;
|
searchProvider?: SearchProviders;
|
||||||
scraperType?: ScraperTypes;
|
scraperType?: ScraperTypes;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue