Search code examples
pythondjangovalidationdjango-formspasswords

Django 5.1 - UserCreationForm won't allow empty passwords


I'm upgrading a Django 3.0 app to 5.1 and have been moving slowly through each minor release. So far so good.

However, once I went from Django 5.0 to 5.1, I saw changed behavior with my "Create New User" page which uses a UserCreationForm form that allows empty passwords. If no password is supplied, a random one is generated.

Now, if I submit the form with an empty password I get "required field" errors on the password fields, even though they are both explicitly set as required=False.

I saw there were UserCreationForm changes in Django 5.1.0 and 5.1.1. I tried using AdminUserCreationForm and setting the usable_password field to None, but it still won't allow empty passwords like before.

Any ideas?

Environment
Python 3.12.8
Django 5.1.5
Crispy Forms 2.3

Simplified Code

from django.contrib.auth.forms import AdminUserCreationForm
from crispy_forms.helper import FormHelper

class SignupForm(AdminUserCreationForm): # previously using UserCreationForm

    usable_password = None # Newly added

    # Form fields
    sharedaccountflag = forms.ChoiceField(
                            label = 'Cuenta compartida', 
                            required = True
                        )

    # Constructor
    def __init__(self, *args, **kwargs):
                
        # Call base class constructor
        super(SignupForm, self).__init__(*args, **kwargs)

        # Set password fields as optional
        self.fields['password1'].required = False
        self.fields['password2'].required = False

        # Set form helper properties
        self.helper = FormHelper()
        self.helper.form_tag = False

    # Specify model and which fields to include in form
    class Meta:
        model = get_user_model()
        fields = ('password1', 'password2', 'sharedaccountflag')

Screenshot
enter image description here


Update
I used Serhii's example and modified it so the normal validation is called when appropriate. I also changed back to use UserCreationForm:

class SignupForm(UserCreationForm):

    # Override default validation to allow empty password (change in Django 5.1)
    def validate_passwords(
        self, 
        password1_field_name = "password1", 
        password2_field_name = "password2"
    ):  
    
        # Store password values
        password1 = self.cleaned_data.get(password1_field_name)  
        password2 = self.cleaned_data.get(password2_field_name)  

        # Do nothing if passwords are not required and no value is provided
        if (
            not self.fields[password1_field_name].required and 
            not self.fields[password2_field_name].required and
            not password1.strip() and 
            not password2.strip()
        ):
            pass

        # Call default validation if password is required OR a value is provided
        else:
            super().validate_passwords(password1_field_name, password2_field_name)

Solution

  • Yes. In new versions of Django, the source code has changed and the behaviour of the BaseUserCreationForm class has changed accordingly. The password1 and password2 fields are now created using the static method SetPasswordMixin.create_password_fields(), and they default to required=False. This can be easily checked here. But even though the fields are optional, the validate_passwords method is always called, in the clean method and checks that the fields are not empty. For example, when you call something like this form.is_valid(), clean will be called.

    If you need behaviour where empty passwords are allowed, given required=False you can define a custom validate_passwords method like the one in the code below, this will allow you to create users with empty passwords:

    from django.contrib.auth import forms
    
    
    class CustomUserCreationForm(forms.UserCreationForm):  
        def validate_passwords(  
                self,  
                password1_field_name: str = "password1",  
                password2_field_name: str = "password2"):  
      
            def is_password_field_required_and_not_valid(field_name: str) -> bool:  
                is_required = self.fields[field_name].required  
                cleaned_value = self.cleaned_data.get(field_name)  
                is_field_has_errors = field_name in self.errors  
                return (  
                    is_required  
                    and not cleaned_value  
                    and not is_field_has_errors  
                )  
      
            if is_password_field_required_and_not_valid(password1_field_name):  
                error = ValidationError(  
                    self.fields[password1_field_name].error_messages["required"],  
                    code="required",  
                )  
                self.add_error(password1_field_name, error)  
      
            if is_password_field_required_and_not_valid(password2_field_name):  
                error = ValidationError(  
                    self.fields[password2_field_name].error_messages["required"],  
                    code="required",  
                )  
                self.add_error(password2_field_name, error)  
      
            password1 = self.cleaned_data.get(password1_field_name)  
            password2 = self.cleaned_data.get(password2_field_name)  
            if password1 != password2:  
                error = ValidationError(  
                    self.error_messages["password_mismatch"],  
                    code="password_mismatch",  
                )  
                self.add_error(password2_field_name, error)
    

    p.s. I'm not sure if that's the way it was intended, maybe there's just a mistake made, in validate_passwords, where the required flag is simply not taken into account.