From ba424666f8730e2fdae0c0b96ecdb24f27411b3e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 27 Aug 2025 16:30:56 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90=20feat:=20Add=20Configurable=20Min?= =?UTF-8?q?.=20Password=20Length=20(#9315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .env.example | 7 +++ api/server/routes/config.js | 5 ++ api/strategies/validators.js | 8 ++- api/strategies/validators.spec.js | 64 +++++++++++++++++++- client/src/components/Auth/LoginForm.tsx | 5 +- client/src/components/Auth/Registration.tsx | 2 +- client/src/components/Auth/ResetPassword.tsx | 4 +- packages/data-provider/src/config.ts | 1 + 8 files changed, 87 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 2b11dc359a..83cbc9c7b2 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,13 @@ NO_INDEX=true # Defaulted to 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 # #===============# diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 9b1c3eee56..67be757f7a 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -117,6 +117,11 @@ router.get('/', async function (req, res) { openidReuseTokens, }; + const minPasswordLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10); + if (minPasswordLength && !isNaN(minPasswordLength)) { + payload.minPasswordLength = minPasswordLength; + } + payload.mcpServers = {}; const getMCPServers = () => { try { diff --git a/api/strategies/validators.js b/api/strategies/validators.js index e8ae300f03..87fa758c38 100644 --- a/api/strategies/validators.js +++ b/api/strategies/validators.js @@ -1,5 +1,7 @@ const { z } = require('zod'); +const MIN_PASSWORD_LENGTH = parseInt(process.env.MIN_PASSWORD_LENGTH, 10) || 8; + const allowedCharactersRegex = new RegExp( '^[' + 'a-zA-Z0-9_.@#$%&*()' + // Basic Latin characters and symbols @@ -32,7 +34,7 @@ const loginSchema = z.object({ email: z.string().email(), password: z .string() - .min(8) + .min(MIN_PASSWORD_LENGTH) .max(128) .refine((value) => value.trim().length > 0, { message: 'Password cannot be only spaces', @@ -50,14 +52,14 @@ const registerSchema = z email: z.string().email(), password: z .string() - .min(8) + .min(MIN_PASSWORD_LENGTH) .max(128) .refine((value) => value.trim().length > 0, { message: 'Password cannot be only spaces', }), confirm_password: z .string() - .min(8) + .min(MIN_PASSWORD_LENGTH) .max(128) .refine((value) => value.trim().length > 0, { message: 'Password cannot be only spaces', diff --git a/api/strategies/validators.spec.js b/api/strategies/validators.spec.js index 312f06923d..b15cf05c69 100644 --- a/api/strategies/validators.spec.js +++ b/api/strategies/validators.spec.js @@ -258,7 +258,7 @@ describe('Zod Schemas', () => { email: 'john@example.com', password: 'password123', confirm_password: 'password123', - extraField: 'I shouldn\'t be here', + extraField: "I shouldn't be here", }); expect(result.success).toBe(true); }); @@ -407,7 +407,7 @@ describe('Zod Schemas', () => { 'john{doe}', // Contains `{` and `}` 'j', // Only one character 'a'.repeat(81), // More than 80 characters - '\' OR \'1\'=\'1\'; --', // SQL Injection + "' OR '1'='1'; --", // SQL Injection '{$ne: null}', // MongoDB Injection '', // Basic XSS '">', // 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)'); }); }); + + 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); + } + }); + }); }); diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index c7a469fc2e..9916ce5653 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -125,7 +125,10 @@ const LoginForm: React.FC = ({ onSubmit, startupConfig, error, aria-label={localize('com_auth_password')} {...register('password', { 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') }, })} aria-invalid={!!errors.password} diff --git a/client/src/components/Auth/Registration.tsx b/client/src/components/Auth/Registration.tsx index 2e915e0137..d80581a8ce 100644 --- a/client/src/components/Auth/Registration.tsx +++ b/client/src/components/Auth/Registration.tsx @@ -165,7 +165,7 @@ const Registration: React.FC = () => { {renderInput('password', 'com_auth_password', 'password', { required: localize('com_auth_password_required'), minLength: { - value: 8, + value: startupConfig?.minPasswordLength || 8, message: localize('com_auth_password_min_length'), }, maxLength: { diff --git a/client/src/components/Auth/ResetPassword.tsx b/client/src/components/Auth/ResetPassword.tsx index 2882e1dc53..6bececb7fe 100644 --- a/client/src/components/Auth/ResetPassword.tsx +++ b/client/src/components/Auth/ResetPassword.tsx @@ -19,7 +19,7 @@ function ResetPassword() { const [params] = useSearchParams(); const password = watch('password'); const resetPassword = useResetPasswordMutation(); - const { setError, setHeaderText } = useOutletContext(); + const { setError, setHeaderText, startupConfig } = useOutletContext(); const onSubmit = (data: TResetPassword) => { resetPassword.mutate(data, { @@ -83,7 +83,7 @@ function ResetPassword() { {...register('password', { required: localize('com_auth_password_required'), minLength: { - value: 8, + value: startupConfig?.minPasswordLength || 8, message: localize('com_auth_password_min_length'), }, maxLength: { diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 12056dab2e..b3763d5260 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -641,6 +641,7 @@ export type TStartupConfig = { sharePointPickerGraphScope?: string; sharePointPickerSharePointScope?: string; openidReuseTokens?: boolean; + minPasswordLength?: number; webSearch?: { searchProvider?: SearchProviders; scraperType?: ScraperTypes;