Test for Password Max length fails with Custom User model

Hello Everyone,

I’ve followed along with a couple of tutorials to write a Custom User model to learn the standard Email as a Username logon pattern. This seems to work fine until I run some crude basic tests against it (first time writing tests in Django so apologies for them being rubbish). I had a look at an existing package: django-username-email but it doesn’t have any obvious test coverage so I am a bit stuck.

My last test (test_password_too_long) fails when I test the max password length and I’m unclear as to why this is happening? I can post the Model, CustomUserCreationForm and CustomUserManager if that helps you check I haven’t done anything weird to the password field (I haven’t honest!).

Any help/pointers as to how I should be testing the max password length would be appreciated.

Thanks
Adam

from django.test import TestCase, override_settings
from django.contrib.auth import get_user_model

from .forms import CustomUserCreationForm


class UsersManagersTests(TestCase):
    def test_create_user(self):
        User = get_user_model()

        user = User.objects.create_user(
            email="test@example.com",
            password="betterpassword",
            first_name="test",
            last_name="ing",
        )
        self.assertEqual(user.email, "test@example.com")
        self.assertEqual(user.first_name, "test")
        self.assertEqual(user.last_name, "ing")
        self.assertTrue(user.is_active)
        self.assertFalse(user.is_superuser)

    def test_create_superuser(self):
        User = get_user_model()

        admin_user = User.objects.create_superuser(
            "super@example.com", "betterpassword"
        )
        self.assertEqual(admin_user.email, "super@example.com")
        self.assertTrue(admin_user.is_active)
        self.assertTrue(admin_user.is_superuser)

    def test_email_already_exists(self):
        User = get_user_model()

        test_user = User.objects.create_user(
            email="test_already_exists@example.com", password="betterpassword"
        )

        data = {
            "email": "test_already_exists@example.com",
            "password1": "betterpassword",
            "password2": "betterpassword",
        }
        form = CustomUserCreationForm(data)
        self.assertFalse(form.is_valid())
        self.assertEqual(
            form["email"].errors, ["User with this Email address already exists."]
        )

    def test_invalid_email(self):
        data = {
            "email": "test_invalid_email!",
            "password1": "betterpassword",
            "password2": "betterpassword",
        }

        form = CustomUserCreationForm(data)
        self.assertFalse(form.is_valid())
        self.assertEqual(form["email"].errors, ["Enter a valid email address."])

    @override_settings(
        AUTH_PASSWORD_VALIDATORS=[
            {
                "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
            },
        ]
    )
    def test_validate_too_similar_password(self):
        data = {
            "email": "test_validate_too_similar@example.com",
            "password1": "test_validate_too_similar@example.com",
            "password2": "test_validate_too_similar@example.com",
        }

        form = CustomUserCreationForm(data)
        self.assertFalse(form.is_valid())
        self.assertEqual(
            form["password2"].errors,
            ["The password is too similar to the email address."],
        )

    @override_settings(
        AUTH_PASSWORD_VALIDATORS=[
            {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
        ]
    )
    def test_validate_too_common_password(self):
        data = {
            "email": "test_validate_too_common@example.com",
            "password1": "password",
            "password2": "password",
        }

        form = CustomUserCreationForm(data)
        self.assertFalse(form.is_valid())
        self.assertEqual(form["password2"].errors, ["This password is too common."])

    @override_settings(
        AUTH_PASSWORD_VALIDATORS=[
            {
                "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"
            },
        ]
    )
    def test_validate_numeric_password(self):
        data = {
            "email": "test_validate_numeric_password@example.com",
            "password1": "12345678",
            "password2": "12345678",
        }

        form = CustomUserCreationForm(data)
        self.assertFalse(form.is_valid())
        self.assertEqual(
            form["password2"].errors, ["This password is entirely numeric."]
        )

    @override_settings(
        AUTH_PASSWORD_VALIDATORS=[
            {
                "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"
            },
        ]
    )
    def test_validate_minimum_length_password(self):
        data = {
            "email": "test_minimum_length_password@example.com",
            "password1": "bad",
            "password2": "bad",
        }

        form = CustomUserCreationForm(data)
        self.assertFalse(form.is_valid())
        self.assertEqual(
            form["password2"].errors,
            ["This password is too short. It must contain at least 8 characters."],
        )

    def test_email_too_long(self):
        data = {
            "email": "b" * 254 + "test_maximum_length_email@example.com",
            "password1": "betterpassword",
            "password2": "betterpassword",
        }

        form = CustomUserCreationForm(data)
        self.assertFalse(form.is_valid())
        self.assertEqual(form.fields["email"].max_length, 254)
        self.assertEqual(
            form["email"].errors,
            ["Ensure this value has at most 254 characters (it has 291)."],
        )

    def test_password_too_long(self):
        data = {
            "email": "test_maximum_length_password@example.com",
            "password1": "betterpassword" * 10,
            "password2": "betterpassword" * 10,
        }

        form = CustomUserCreationForm(data)
        # Should fail here but doesn't. Why?... no validator for password being longer than max length?
        self.assertFalse(form.is_valid())
        self.assertEqual(form.fields["password2"].max_length, 127)
        self.assertEqual(
            form["password2"].errors,
            ["Ensure this value has at most 127 characters (it has 140)."],
        )

Hello,
Sorry for answering my own question but after a quick test it appears that vanilla Django doesn’t care what password length a user puts in so I was testing under the incorrect assumption that I had broken it.

I had seen the code below from django.contrib.auth.base_user and assumed incorrectly that there was a max length password validator somewhere that I had failed to implement:

class AbstractBaseUser(models.Model):
    password = models.CharField(_('password'), max_length=128)

After a bit of reading I now understand that the password is stored as a hash and that is what the max_length=128 above is referring to not the real password max length. So I will have to write my own max password length validator as it appears that there is no limit to what a malicious user can chuck in there.

Cheers

Adam

The more common way to handle this would be to add the limit / validation to the form where the user enters the password.

But since you’re not storing the plain-text password anywhere, what’s the problem with someone entering a 200-character password?

Thank you for your response. I’m so new to Django that I didn’t realise that is the proper approach I assumed incorrectly that I should be checking using the built in password validators and that I had broken it by sub classing.

No problem with a user entering a 200-character password whatsoever but there is if there is no check on the size of password and you end up hashing lots of large blobs of data that someone decides to maliciously post to the app. This is an easy vector to perform a distributed denial of service attack although I am sure there is a limit somewhere I just don’t know enough about Django to find it.

After a bit of head scratching and rummaging around in the documentation I found that I could write a custom password validator and this would solve my issue. Here it is if it is any use to someone. This goes in the AUTH_PASSWORD_VALIDATORS section in settings.py file:

{

        'NAME': 'users.validators.MaximumLengthValidator',

            'OPTIONS': {

                'max_length': 200, }

    },

And here is the code that goes in validators.py or whatever you want to call it.

from django.core.exceptions import ValidationError

from django.utils.translation import gettext as _

class MaximumLengthValidator:

    def __init__(self, max_length=200):

        self.max_length = max_length

    def validate(self, password, user=None):

        if len(password) > self.max_length:

            raise ValidationError(

                _("This password is greater than the maximum of %(max_length)d characters."),

                code='password_too_long',

                params={'max_length': self.max_length},

            )

    def get_help_text(self):

        return _(

            "Your password can be a maximum of %(max_length)d characters."

            % {'max_length': self.max_length}

        ) 
1 Like