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;