diff --git a/invokeai/app/api/routers/auth.py b/invokeai/app/api/routers/auth.py
index 2e7e49c41e3..b4c1e86cf33 100644
--- a/invokeai/app/api/routers/auth.py
+++ b/invokeai/app/api/routers/auth.py
@@ -79,6 +79,7 @@ class SetupStatusResponse(BaseModel):
setup_required: bool = Field(description="Whether initial setup is required")
multiuser_enabled: bool = Field(description="Whether multiuser mode is enabled")
+ strict_password_checking: bool = Field(description="Whether strict password requirements are enforced")
@auth_router.get("/status", response_model=SetupStatusResponse)
@@ -92,13 +93,17 @@ async def get_setup_status() -> SetupStatusResponse:
# If multiuser is disabled, setup is never required
if not config.multiuser:
- return SetupStatusResponse(setup_required=False, multiuser_enabled=False)
+ return SetupStatusResponse(
+ setup_required=False, multiuser_enabled=False, strict_password_checking=config.strict_password_checking
+ )
# In multiuser mode, check if an admin exists
user_service = ApiDependencies.invoker.services.users
setup_required = not user_service.has_admin()
- return SetupStatusResponse(setup_required=setup_required, multiuser_enabled=True)
+ return SetupStatusResponse(
+ setup_required=setup_required, multiuser_enabled=True, strict_password_checking=config.strict_password_checking
+ )
@auth_router.post("/login", response_model=LoginResponse)
@@ -248,7 +253,7 @@ async def setup_admin(
password=request.password,
is_admin=True,
)
- user = user_service.create_admin(user_data)
+ user = user_service.create_admin(user_data, strict_password_checking=config.strict_password_checking)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
@@ -359,6 +364,7 @@ async def create_user(
HTTPException: 400 if email already exists or password is weak
"""
user_service = ApiDependencies.invoker.services.users
+ config = ApiDependencies.invoker.services.configuration
try:
user_data = UserCreateRequest(
email=request.email,
@@ -366,7 +372,7 @@ async def create_user(
password=request.password,
is_admin=request.is_admin,
)
- return user_service.create(user_data)
+ return user_service.create(user_data, strict_password_checking=config.strict_password_checking)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
@@ -414,6 +420,7 @@ async def update_user(
HTTPException: 404 if user not found
"""
user_service = ApiDependencies.invoker.services.users
+ config = ApiDependencies.invoker.services.configuration
try:
changes = UserUpdateRequest(
display_name=request.display_name,
@@ -421,7 +428,7 @@ async def update_user(
is_admin=request.is_admin,
is_active=request.is_active,
)
- return user_service.update(user_id, changes)
+ return user_service.update(user_id, changes, strict_password_checking=config.strict_password_checking)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
@@ -483,6 +490,7 @@ async def update_current_user(
HTTPException: 404 if user not found
"""
user_service = ApiDependencies.invoker.services.users
+ config = ApiDependencies.invoker.services.configuration
# Verify current password when attempting a password change
if request.new_password is not None:
@@ -509,6 +517,8 @@ async def update_current_user(
display_name=request.display_name,
password=request.new_password,
)
- return user_service.update(current_user.user_id, changes)
+ return user_service.update(
+ current_user.user_id, changes, strict_password_checking=config.strict_password_checking
+ )
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
diff --git a/invokeai/app/services/auth/password_utils.py b/invokeai/app/services/auth/password_utils.py
index 5e641516347..b960af5f1c5 100644
--- a/invokeai/app/services/auth/password_utils.py
+++ b/invokeai/app/services/auth/password_utils.py
@@ -1,6 +1,6 @@
"""Password hashing and validation utilities."""
-from typing import cast
+from typing import Literal, cast
from passlib.context import CryptContext
@@ -84,3 +84,30 @@ def validate_password_strength(password: str) -> tuple[bool, str]:
return False, "Password must contain uppercase, lowercase, and numbers"
return True, ""
+
+
+def get_password_strength(password: str) -> Literal["weak", "moderate", "strong"]:
+ """Determine the strength of a password.
+
+ Strength levels:
+ - weak: less than 8 characters
+ - moderate: 8+ characters but missing at least one of uppercase, lowercase, or digit
+ - strong: 8+ characters with uppercase, lowercase, and digit
+
+ Args:
+ password: The password to evaluate
+
+ Returns:
+ One of "weak", "moderate", or "strong"
+ """
+ if len(password) < 8:
+ return "weak"
+
+ has_upper = any(c.isupper() for c in password)
+ has_lower = any(c.islower() for c in password)
+ has_digit = any(c.isdigit() for c in password)
+
+ if not (has_upper and has_lower and has_digit):
+ return "moderate"
+
+ return "strong"
diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py
index 2cc2aaf273c..5d1b1d0d8d5 100644
--- a/invokeai/app/services/config/config_default.py
+++ b/invokeai/app/services/config/config_default.py
@@ -111,6 +111,7 @@ class InvokeAIAppConfig(BaseSettings):
unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.
allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.
multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.
+ strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.
"""
_root: Optional[Path] = PrivateAttr(default=None)
@@ -206,6 +207,7 @@ class InvokeAIAppConfig(BaseSettings):
# MULTIUSER
multiuser: bool = Field(default=False, description="Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.")
+ strict_password_checking: bool = Field(default=False, description="Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.")
# fmt: on
diff --git a/invokeai/app/services/users/users_base.py b/invokeai/app/services/users/users_base.py
index 5ad66c59832..728a0adfa37 100644
--- a/invokeai/app/services/users/users_base.py
+++ b/invokeai/app/services/users/users_base.py
@@ -9,17 +9,19 @@ class UserServiceBase(ABC):
"""High-level service for user management."""
@abstractmethod
- def create(self, user_data: UserCreateRequest) -> UserDTO:
+ def create(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
"""Create a new user.
Args:
user_data: User creation data
+ strict_password_checking: If True (default), passwords must meet strength requirements.
+ If False, any non-empty password is accepted.
Returns:
The created user
Raises:
- ValueError: If email already exists or password is weak
+ ValueError: If email already exists or (when strict) password is weak
"""
pass
@@ -48,18 +50,20 @@ def get_by_email(self, email: str) -> UserDTO | None:
pass
@abstractmethod
- def update(self, user_id: str, changes: UserUpdateRequest) -> UserDTO:
+ def update(self, user_id: str, changes: UserUpdateRequest, strict_password_checking: bool = True) -> UserDTO:
"""Update user.
Args:
user_id: The user ID
changes: Fields to update
+ strict_password_checking: If True (default), passwords must meet strength requirements.
+ If False, any non-empty password is accepted.
Returns:
The updated user
Raises:
- ValueError: If user not found or password is weak
+ ValueError: If user not found or (when strict) password is weak
"""
pass
@@ -98,17 +102,19 @@ def has_admin(self) -> bool:
pass
@abstractmethod
- def create_admin(self, user_data: UserCreateRequest) -> UserDTO:
+ def create_admin(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
"""Create an admin user (for initial setup).
Args:
user_data: User creation data
+ strict_password_checking: If True (default), passwords must meet strength requirements.
+ If False, any non-empty password is accepted.
Returns:
The created admin user
Raises:
- ValueError: If admin already exists or password is weak
+ ValueError: If admin already exists or (when strict) password is weak
"""
pass
diff --git a/invokeai/app/services/users/users_default.py b/invokeai/app/services/users/users_default.py
index 506ae937f02..709e4cb82c6 100644
--- a/invokeai/app/services/users/users_default.py
+++ b/invokeai/app/services/users/users_default.py
@@ -21,12 +21,15 @@ def __init__(self, db: SqliteDatabase):
"""
self._db = db
- def create(self, user_data: UserCreateRequest) -> UserDTO:
+ def create(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
"""Create a new user."""
# Validate password strength
- is_valid, error_msg = validate_password_strength(user_data.password)
- if not is_valid:
- raise ValueError(error_msg)
+ if strict_password_checking:
+ is_valid, error_msg = validate_password_strength(user_data.password)
+ if not is_valid:
+ raise ValueError(error_msg)
+ elif not user_data.password:
+ raise ValueError("Password cannot be empty")
# Check if email already exists
if self.get_by_email(user_data.email) is not None:
@@ -106,7 +109,7 @@ def get_by_email(self, email: str) -> UserDTO | None:
last_login_at=datetime.fromisoformat(row[7]) if row[7] else None,
)
- def update(self, user_id: str, changes: UserUpdateRequest) -> UserDTO:
+ def update(self, user_id: str, changes: UserUpdateRequest, strict_password_checking: bool = True) -> UserDTO:
"""Update user."""
# Check if user exists
user = self.get(user_id)
@@ -115,9 +118,12 @@ def update(self, user_id: str, changes: UserUpdateRequest) -> UserDTO:
# Validate password if provided
if changes.password is not None:
- is_valid, error_msg = validate_password_strength(changes.password)
- if not is_valid:
- raise ValueError(error_msg)
+ if strict_password_checking:
+ is_valid, error_msg = validate_password_strength(changes.password)
+ if not is_valid:
+ raise ValueError(error_msg)
+ elif not changes.password:
+ raise ValueError("Password cannot be empty")
# Build update query dynamically based on provided fields
updates: list[str] = []
@@ -208,7 +214,7 @@ def has_admin(self) -> bool:
count = row[0] if row else 0
return bool(count > 0)
- def create_admin(self, user_data: UserCreateRequest) -> UserDTO:
+ def create_admin(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
"""Create an admin user (for initial setup)."""
if self.has_admin():
raise ValueError("Admin user already exists")
@@ -220,7 +226,7 @@ def create_admin(self, user_data: UserCreateRequest) -> UserDTO:
password=user_data.password,
is_admin=True,
)
- return self.create(admin_data)
+ return self.create(admin_data, strict_password_checking=strict_password_checking)
def list_users(self, limit: int = 100, offset: int = 0) -> list[UserDTO]:
"""List all users."""
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 617c4341574..58be5430a26 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -46,7 +46,8 @@
"passwordsDoNotMatch": "Passwords do not match",
"createAccount": "Create Administrator Account",
"creatingAccount": "Setting up...",
- "setupFailed": "Setup failed. Please try again."
+ "setupFailed": "Setup failed. Please try again.",
+ "passwordHelperRelaxed": "Enter any password (strength will be shown)"
},
"userMenu": "User Menu",
"admin": "Admin",
@@ -102,6 +103,11 @@
"back": "Back",
"cannotDeleteSelf": "You cannot delete your own account",
"cannotDeactivateSelf": "You cannot deactivate your own account"
+ },
+ "passwordStrength": {
+ "weak": "Weak password",
+ "moderate": "Moderate password",
+ "strong": "Strong password"
}
},
"boards": {
diff --git a/invokeai/frontend/web/src/features/auth/components/AdministratorSetup.tsx b/invokeai/frontend/web/src/features/auth/components/AdministratorSetup.tsx
index 9827a4d9769..b0ad9a5e047 100644
--- a/invokeai/frontend/web/src/features/auth/components/AdministratorSetup.tsx
+++ b/invokeai/frontend/web/src/features/auth/components/AdministratorSetup.tsx
@@ -15,34 +15,13 @@ import {
Text,
VStack,
} from '@invoke-ai/ui-library';
+import { validatePasswordField } from 'features/auth/util/passwordUtils';
import type { ChangeEvent, FormEvent } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useGetSetupStatusQuery, useSetupMutation } from 'services/api/endpoints/auth';
-const validatePasswordStrength = (
- password: string,
- t: (key: string) => string
-): { isValid: boolean; message: string } => {
- if (password.length < 8) {
- return { isValid: false, message: t('auth.setup.passwordTooShort') };
- }
-
- const hasUpper = /[A-Z]/.test(password);
- const hasLower = /[a-z]/.test(password);
- const hasDigit = /\d/.test(password);
-
- if (!hasUpper || !hasLower || !hasDigit) {
- return {
- isValid: false,
- message: t('auth.setup.passwordMissingRequirements'),
- };
- }
-
- return { isValid: true, message: '' };
-};
-
export const AdministratorSetup = memo(() => {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -60,7 +39,8 @@ export const AdministratorSetup = memo(() => {
}
}, [setupStatus, isLoadingSetup, navigate]);
- const passwordValidation = validatePasswordStrength(password, t);
+ const strictPasswordChecking = setupStatus?.strict_password_checking ?? true;
+ const passwordValidation = validatePasswordField(password, t, strictPasswordChecking, false);
const passwordsMatch = password === confirmPassword;
const handleSubmit = useCallback(
@@ -120,6 +100,13 @@ export const AdministratorSetup = memo(() => {
);
}
+ const passwordStrengthColor =
+ passwordValidation.strength === 'weak'
+ ? 'error.300'
+ : passwordValidation.strength === 'moderate'
+ ? 'warning.300'
+ : 'invokeBlue.300';
+
return (
@@ -192,7 +179,16 @@ export const AdministratorSetup = memo(() => {
{password.length > 0 && !passwordValidation.isValid && (
{passwordValidation.message}
)}
- {password.length === 0 && {t('auth.setup.passwordHelper')}}
+ {password.length > 0 && passwordValidation.isValid && passwordValidation.message && (
+
+ {passwordValidation.message}
+
+ )}
+ {password.length === 0 && (
+
+ {strictPasswordChecking ? t('auth.setup.passwordHelper') : t('auth.setup.passwordHelperRelaxed')}
+
+ )}
diff --git a/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx b/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx
index 4dd88ca1e5a..8d587e7249e 100644
--- a/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx
+++ b/invokeai/frontend/web/src/features/auth/components/UserManagement.tsx
@@ -37,6 +37,7 @@ import {
} from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCurrentUser } from 'features/auth/store/authSlice';
+import { validatePasswordField } from 'features/auth/util/passwordUtils';
import type { ChangeEvent, FormEvent } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -54,30 +55,12 @@ import type { UserDTO } from 'services/api/endpoints/auth';
import {
useCreateUserMutation,
useDeleteUserMutation,
+ useGetSetupStatusQuery,
useLazyGeneratePasswordQuery,
useListUsersQuery,
useUpdateUserMutation,
} from 'services/api/endpoints/auth';
-const validatePasswordStrength = (
- password: string,
- t: (key: string) => string
-): { isValid: boolean; message: string } => {
- if (password.length === 0) {
- return { isValid: true, message: '' };
- }
- if (password.length < 8) {
- return { isValid: false, message: t('auth.setup.passwordTooShort') };
- }
- const hasUpper = /[A-Z]/.test(password);
- const hasLower = /[a-z]/.test(password);
- const hasDigit = /\d/.test(password);
- if (!hasUpper || !hasLower || !hasDigit) {
- return { isValid: false, message: t('auth.setup.passwordMissingRequirements') };
- }
- return { isValid: true, message: '' };
-};
-
const FORM_GRID_COLUMNS = '120px 1fr';
// ---------------------------------------------------------------------------
@@ -105,9 +88,12 @@ const UserFormModal = memo(({ isOpen, onClose, editUser }: UserFormModalProps) =
const [createUser, { isLoading: isCreating }] = useCreateUserMutation();
const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation();
const [triggerGeneratePassword] = useLazyGeneratePasswordQuery();
+ const { data: setupStatus } = useGetSetupStatusQuery();
const isLoading = isCreating || isUpdating;
- const passwordValidation = validatePasswordStrength(password, t);
+ const strictPasswordChecking = setupStatus?.strict_password_checking ?? true;
+ // In edit mode, empty password means "no change" (allowEmpty=true); in create mode password is required (allowEmpty=false)
+ const passwordValidation = validatePasswordField(password, t, strictPasswordChecking, isEdit);
const handleGeneratePassword = useCallback(async () => {
try {
@@ -300,6 +286,21 @@ const UserFormModal = memo(({ isOpen, onClose, editUser }: UserFormModalProps) =
{password.length > 0 && !passwordValidation.isValid && (
{passwordValidation.message}
)}
+ {password.length > 0 && passwordValidation.isValid && passwordValidation.message && (
+
+ {passwordValidation.message}
+
+ )}
diff --git a/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx b/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx
index 4504698f0ea..02d25b6de98 100644
--- a/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx
+++ b/invokeai/frontend/web/src/features/auth/components/UserProfile.tsx
@@ -21,31 +21,17 @@ import {
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectAuthToken, selectCurrentUser, setCredentials } from 'features/auth/store/authSlice';
+import { validatePasswordField } from 'features/auth/util/passwordUtils';
import type { ChangeEvent, FormEvent } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiEyeBold, PiEyeSlashBold, PiLightningFill } from 'react-icons/pi';
import { useNavigate } from 'react-router-dom';
-import { useLazyGeneratePasswordQuery, useUpdateCurrentUserMutation } from 'services/api/endpoints/auth';
-
-const validatePasswordStrength = (
- password: string,
- t: (key: string) => string
-): { isValid: boolean; message: string } => {
- if (password.length === 0) {
- return { isValid: true, message: '' };
- }
- if (password.length < 8) {
- return { isValid: false, message: t('auth.setup.passwordTooShort') };
- }
- const hasUpper = /[A-Z]/.test(password);
- const hasLower = /[a-z]/.test(password);
- const hasDigit = /\d/.test(password);
- if (!hasUpper || !hasLower || !hasDigit) {
- return { isValid: false, message: t('auth.setup.passwordMissingRequirements') };
- }
- return { isValid: true, message: '' };
-};
+import {
+ useGetSetupStatusQuery,
+ useLazyGeneratePasswordQuery,
+ useUpdateCurrentUserMutation,
+} from 'services/api/endpoints/auth';
const PASSWORD_GRID_COLUMNS = '180px 1fr';
@@ -67,8 +53,10 @@ export const UserProfile = memo(() => {
const [updateCurrentUser, { isLoading }] = useUpdateCurrentUserMutation();
const [triggerGeneratePassword] = useLazyGeneratePasswordQuery();
+ const { data: setupStatus } = useGetSetupStatusQuery();
- const newPasswordValidation = validatePasswordStrength(newPassword, t);
+ const strictPasswordChecking = setupStatus?.strict_password_checking ?? true;
+ const newPasswordValidation = validatePasswordField(newPassword, t, strictPasswordChecking, true);
const isPasswordChangeAttempted = newPassword.length > 0 || currentPassword.length > 0;
const passwordsMatch = newPassword.length > 0 && newPassword === confirmPassword;
@@ -305,6 +293,21 @@ export const UserProfile = memo(() => {
{newPassword.length > 0 && !newPasswordValidation.isValid && (
{newPasswordValidation.message}
)}
+ {newPassword.length > 0 && newPasswordValidation.isValid && newPasswordValidation.message && (
+
+ {newPasswordValidation.message}
+
+ )}
diff --git a/invokeai/frontend/web/src/features/auth/util/passwordUtils.ts b/invokeai/frontend/web/src/features/auth/util/passwordUtils.ts
new file mode 100644
index 00000000000..53200d2c65f
--- /dev/null
+++ b/invokeai/frontend/web/src/features/auth/util/passwordUtils.ts
@@ -0,0 +1,70 @@
+export type PasswordStrength = 'weak' | 'moderate' | 'strong';
+
+export type PasswordValidationResult = {
+ isValid: boolean;
+ message: string;
+ strength: PasswordStrength | null;
+};
+
+/**
+ * Returns the strength level of a password.
+ * - weak: less than 8 characters
+ * - moderate: 8+ characters but missing uppercase, lowercase, or digit
+ * - strong: 8+ characters with uppercase, lowercase, and digit
+ */
+export const getPasswordStrength = (password: string): PasswordStrength => {
+ if (password.length < 8) {
+ return 'weak';
+ }
+ const hasUpper = /[A-Z]/.test(password);
+ const hasLower = /[a-z]/.test(password);
+ const hasDigit = /\d/.test(password);
+ if (!hasUpper || !hasLower || !hasDigit) {
+ return 'moderate';
+ }
+ return 'strong';
+};
+
+/**
+ * Validates a password field.
+ *
+ * In strict mode, passwords must be 8+ characters with uppercase, lowercase, and digits.
+ * In non-strict mode, any non-empty password is accepted but strength is reported.
+ *
+ * @param password - The password to validate
+ * @param t - Translation function
+ * @param strictPasswordChecking - Whether to enforce strict requirements
+ * @param allowEmpty - When true, an empty string is treated as "no change" (valid with no message)
+ */
+export const validatePasswordField = (
+ password: string,
+ t: (key: string) => string,
+ strictPasswordChecking: boolean,
+ allowEmpty = false
+): PasswordValidationResult => {
+ if (password.length === 0) {
+ return { isValid: allowEmpty, message: '', strength: null };
+ }
+
+ const strength = getPasswordStrength(password);
+
+ if (!strictPasswordChecking) {
+ return {
+ isValid: true,
+ message: t(`auth.passwordStrength.${strength}`),
+ strength,
+ };
+ }
+
+ // Strict mode
+ if (password.length < 8) {
+ return { isValid: false, message: t('auth.setup.passwordTooShort'), strength };
+ }
+ const hasUpper = /[A-Z]/.test(password);
+ const hasLower = /[a-z]/.test(password);
+ const hasDigit = /\d/.test(password);
+ if (!hasUpper || !hasLower || !hasDigit) {
+ return { isValid: false, message: t('auth.setup.passwordMissingRequirements'), strength };
+ }
+ return { isValid: true, message: '', strength };
+};
diff --git a/invokeai/frontend/web/src/services/api/endpoints/auth.ts b/invokeai/frontend/web/src/services/api/endpoints/auth.ts
index c7a8a8b1ffc..419e7c730ce 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/auth.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/auth.ts
@@ -33,6 +33,7 @@ type LogoutResponse = {
type SetupStatusResponse = {
setup_required: boolean;
multiuser_enabled: boolean;
+ strict_password_checking: boolean;
};
export type UserDTO = components['schemas']['UserDTO'];
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index 52a318f8160..2f6af1ee2e5 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -14375,6 +14375,7 @@ export type components = {
* unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.
* allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.
* multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.
+ * strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.
*/
InvokeAIAppConfig: {
/**
@@ -14748,6 +14749,12 @@ export type components = {
* @default false
*/
multiuser?: boolean;
+ /**
+ * Strict Password Checking
+ * @description Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.
+ * @default false
+ */
+ strict_password_checking?: boolean;
};
/**
* InvokeAIAppConfigWithSetFields
@@ -24486,6 +24493,11 @@ export type components = {
* @description Whether multiuser mode is enabled
*/
multiuser_enabled: boolean;
+ /**
+ * Strict Password Checking
+ * @description Whether strict password requirements are enforced
+ */
+ strict_password_checking: boolean;
};
/**
* Show Image
diff --git a/tests/app/routers/test_auth.py b/tests/app/routers/test_auth.py
index 0949048e607..5362bd775ff 100644
--- a/tests/app/routers/test_auth.py
+++ b/tests/app/routers/test_auth.py
@@ -300,8 +300,9 @@ def test_setup_admin_already_exists(monkeypatch: Any, mock_invoker: Invoker, cli
def test_setup_admin_weak_password(monkeypatch: Any, mock_invoker: Invoker, client: TestClient) -> None:
- """Test setup fails with weak password."""
+ """Test setup fails with weak password when strict password checking is enabled."""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
+ mock_invoker.services.configuration.strict_password_checking = True
response = client.post(
"/api/v1/auth/setup",
@@ -316,6 +317,25 @@ def test_setup_admin_weak_password(monkeypatch: Any, mock_invoker: Invoker, clie
assert "Password" in response.json()["detail"]
+def test_setup_admin_weak_password_non_strict(monkeypatch: Any, mock_invoker: Invoker, client: TestClient) -> None:
+ """Test setup succeeds with weak password when strict password checking is disabled (the default)."""
+ monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
+ mock_invoker.services.configuration.strict_password_checking = False
+
+ response = client.post(
+ "/api/v1/auth/setup",
+ json={
+ "email": "admin3b@example.com",
+ "display_name": "Admin User",
+ "password": "weak",
+ },
+ )
+
+ assert response.status_code == 200
+ json_response = response.json()
+ assert json_response["success"] is True
+
+
def test_admin_user_token_has_admin_flag(monkeypatch: Any, mock_invoker: Invoker, client: TestClient) -> None:
"""Test that admin user login returns token with admin flag."""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
diff --git a/tests/app/services/auth/test_password_utils.py b/tests/app/services/auth/test_password_utils.py
index 64fdeb9d424..82b1c435ef0 100644
--- a/tests/app/services/auth/test_password_utils.py
+++ b/tests/app/services/auth/test_password_utils.py
@@ -1,6 +1,11 @@
"""Unit tests for password utilities."""
-from invokeai.app.services.auth.password_utils import hash_password, validate_password_strength, verify_password
+from invokeai.app.services.auth.password_utils import (
+ get_password_strength,
+ hash_password,
+ validate_password_strength,
+ verify_password,
+)
class TestPasswordHashing:
@@ -223,6 +228,58 @@ def test_validate_password_very_long(self):
assert message == ""
+class TestGetPasswordStrength:
+ """Tests for get_password_strength function."""
+
+ def test_weak_password_too_short(self):
+ """Test that passwords shorter than 8 characters are 'weak'."""
+ assert get_password_strength("Ab1") == "weak"
+ assert get_password_strength("Ab1defg") == "weak" # 7 chars
+ assert get_password_strength("") == "weak"
+
+ def test_moderate_password_missing_uppercase(self):
+ """Test that 8+ char passwords missing uppercase are 'moderate'."""
+ assert get_password_strength("lowercase1") == "moderate"
+
+ def test_moderate_password_missing_lowercase(self):
+ """Test that 8+ char passwords missing lowercase are 'moderate'."""
+ assert get_password_strength("UPPERCASE1") == "moderate"
+
+ def test_moderate_password_missing_digit(self):
+ """Test that 8+ char passwords missing digits are 'moderate'."""
+ assert get_password_strength("NoDigitsHere") == "moderate"
+
+ def test_moderate_password_only_lowercase_and_digit(self):
+ """Test that 8+ char passwords with only lowercase and digit are 'moderate'."""
+ assert get_password_strength("lowercase1") == "moderate"
+
+ def test_strong_password(self):
+ """Test that 8+ char passwords with upper, lower, and digit are 'strong'."""
+ assert get_password_strength("StrongPass1") == "strong"
+ assert get_password_strength("Pass123A") == "strong"
+
+ def test_strong_password_with_special_chars(self):
+ """Test that passwords meeting all requirements plus special chars are 'strong'."""
+ assert get_password_strength("Pass!@#$123") == "strong"
+
+ def test_exactly_8_characters_meeting_requirements(self):
+ """Test that exactly 8 characters meeting requirements is 'strong'."""
+ assert get_password_strength("Pass123A") == "strong"
+
+ def test_exactly_8_characters_missing_uppercase(self):
+ """Test that exactly 8 characters missing uppercase is 'moderate'."""
+ assert get_password_strength("pass123a") == "moderate"
+
+ def test_strength_progression(self):
+ """Test that strength improves as requirements are met."""
+ # Too short - weak
+ assert get_password_strength("Abc1") == "weak"
+ # Long enough but only lowercase - moderate
+ assert get_password_strength("abcdefgh") == "moderate"
+ # Meets all requirements - strong
+ assert get_password_strength("Abcdefg1") == "strong"
+
+
class TestPasswordSecurityProperties:
"""Tests for security properties of password handling."""
diff --git a/tests/app/services/users/test_user_service.py b/tests/app/services/users/test_user_service.py
index 479c911a0da..d5d04964005 100644
--- a/tests/app/services/users/test_user_service.py
+++ b/tests/app/services/users/test_user_service.py
@@ -62,7 +62,7 @@ def test_create_user(user_service: UserService):
def test_create_user_weak_password(user_service: UserService):
- """Test creating a user with weak password."""
+ """Test creating a user with weak password fails when strict checking is enabled."""
user_data = UserCreateRequest(
email="test@example.com",
display_name="Test User",
@@ -71,7 +71,20 @@ def test_create_user_weak_password(user_service: UserService):
)
with pytest.raises(ValueError, match="at least 8 characters"):
- user_service.create(user_data)
+ user_service.create(user_data, strict_password_checking=True)
+
+
+def test_create_user_weak_password_non_strict(user_service: UserService):
+ """Test creating a user with weak password succeeds when strict checking is disabled."""
+ user_data = UserCreateRequest(
+ email="weakpass@example.com",
+ display_name="Test User",
+ password="weak",
+ is_admin=False,
+ )
+
+ user = user_service.create(user_data, strict_password_checking=False)
+ assert user.email == "weakpass@example.com"
def test_create_duplicate_user(user_service: UserService):