0

This is a followup to this question: Official advice for printing all Django form errors in a template not working...or not understood

I set up these two users in my Django app: admin/admin and b/b (user/pass). Here's the login form:

{% load i18n %}       {# For the "trans" tag #}
<!DOCTYPE html>
<html lang="en">
<HTML><HEAD>
   <TITLE>Login</TITLE>
</HEAD>

<BODY>

<H1>Login</H1>

<form method="post" id="loginForm" action="{% url 'login' %}">
{% csrf_token %}
---{{ form.as_p }}===
  <label><input name="remember" type="checkbox">{% trans "Remember me" %}</label>

  <input type="submit" value="login" />
  <input type="hidden" name="next" value="{% url 'main_page' %}" />
</form>

</BODY></HTML>

I can login with either account, no problem. An invalid user/pass combination error is correctly printed by form.as_p:

screenshot

Now, I want to enforce min/max lengths for both the username and password. I'm configuring these values in models.py:

USERNAME_MIN_LEN = 5
"""
The database allows one-character usernames. We're going to forbid
anything less than five characters.
"""
USERNAME_MAX_LEN = User._meta.get_field('username').max_length
"""
The maximum allowable username length, as determined by the database
column. Equal to
    `User._meta.get_field('username').max_length`
"""
PASSWORD_MIN_LEN = 5
"""
The password is stored in the database, not as plain text, but as it's
generated hash. Length is therefore not enforced by the database at all.
We're going to minimally protect users against themselves and impose an
five character minimum. (For real, I'd make this eight. When testing, I
make the password equal to the username, so it's temporarily shorter.)
"""
PASSWORD_MAX_LEN = 4096
"""
Imposing a maximum password length is not recommended:
- https://stackoverflow.com/questions/98768/should-i-impose-a-maximum-length-on-passwords

However, Django prevents an attack vector by forbidding excessively-long
passwords (See "Issue: denial-of-service via large passwords"):
- https://www.djangoproject.com/weblog/2013/sep/15/security/
"""

And using them in an sub-class of django.contrib.auth.forms import AuthenticationForm:

def get_min_max_incl_err_msg(min_int, max_int):
    """A basic error message for inclusive string length."""
    "Must be between " + str(min_int) + " and " + str(max_int) + " characters, inclusive."

username_min_max_len_err_msg = get_min_max_incl_err_msg(USERNAME_MIN_LEN, USERNAME_MAX_LEN)
pwd_min_max_len_err_msg = get_min_max_incl_err_msg(PASSWORD_MIN_LEN, PASSWORD_MAX_LEN)

class AuthenticationFormEnforceLength(AuthenticationForm):
    """
    An `AuthenticationForm` that enforces min/max lengths.
    - https://docs.djangoproject.com/en/1.7/_modules/django/contrib/auth/forms/#AuthenticationForm

    Pass this into the login form via the `authentication_form` parameter.
    - https://docs.djangoproject.com/en/1.7/topics/auth/default/#django.contrib.auth.views.login
    Which is done in `registration/urls.py`.
    """
    username = forms.CharField(min_length=USERNAME_MIN_LEN,
                               max_length=USERNAME_MAX_LEN,
                               error_messages={
                                   'min_length': username_min_max_len_err_msg,
                                   'max_length': username_min_max_len_err_msg })
    password = forms.CharField(label=_("Password"), widget=forms.PasswordInput,
                                    min_length=PASSWORD_MIN_LEN,
                                    max_length=PASSWORD_MAX_LEN,
                                    error_messages={
                                        'min_length': pwd_min_max_len_err_msg,
                                        'max_length': pwd_min_max_len_err_msg })

Here's the url entry:

from auth_lifecycle.registration.view_login import AuthenticationFormEnforceLength

....

url(r"^login/$",
   "auth_lifecycle.registration.view_login.login_maybe_remember",
   { "authentication_form": AuthenticationFormEnforceLength },
   name="login"),

As expected, logging in with admin/admin still works, and b/b now fails. However, it's not printing out any bad-length error. Instead, it's crashing with

TypeError at /auth/login/ ... unsupported operand type(s) for %=: 'NoneType' and 'dict'

I thought it might be getting confused with "good login but bad length", but this same error occurs with a totally bogus user/pass, such as x/x. The "NoneType" implies no error, but there clearly is an error...right?

How do I get this "bad-length" message to print, and what is causing this TypeError?


The entire view_login.py, which includes the login view and the form object (the extra context variables are used by JavaScript, which I've stripped out of the above template):

from auth_lifecycle.models     import PASSWORD_MIN_LEN, PASSWORD_MAX_LEN
from auth_lifecycle.models     import USERNAME_MIN_LEN, USERNAME_MAX_LEN
from django                    import forms    #NOT django.contrib.auth.forms
#from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.views import login
from django.core.exceptions    import ValidationError
from django.utils.translation  import ugettext, ugettext_lazy as _

def login_maybe_remember(request, *args, **kwargs):
    """
    Login with remember-me functionality and length checking. If the
    remember-me checkbox is checked, the session is remembered for
    SESSION_COOKIE_AGE seconds. If unchecked, the session expires at
    browser close.

    - https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-SESSION_COOKIE_AGE
    - https://docs.djangoproject.com/en/1.7/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.set_expiry
    - https://docs.djangoproject.com/en/1.7/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.get_expire_at_browser_close
    """
    if request.method == 'POST' and not request.POST.get('remember', None):
        #This is a login attempt and the checkbox is not checked.
        request.session.set_expiry(0)

    context = {}
    context["USERNAME_MIN_LEN"] = USERNAME_MIN_LEN
    context["USERNAME_MAX_LEN"] = USERNAME_MAX_LEN
    context["PASSWORD_MIN_LEN"] = PASSWORD_MIN_LEN
    context["PASSWORD_MAX_LEN"] = PASSWORD_MAX_LEN
    kwargs["extra_context"] = context

    return login(request, *args, **kwargs)

def get_min_max_incl_err_msg(min_int, max_int):
    """A basic error message for inclusive string length."""
    "Must be between " + str(min_int) + " and " + str(max_int) + " characters, inclusive."

username_min_max_len_err_msg = get_min_max_incl_err_msg(USERNAME_MIN_LEN, USERNAME_MAX_LEN)
pwd_min_max_len_err_msg = get_min_max_incl_err_msg(PASSWORD_MIN_LEN, PASSWORD_MAX_LEN)

class AuthenticationFormEnforceLength(AuthenticationForm):
    """
    An `AuthenticationForm` that enforces min/max lengths.
    - https://docs.djangoproject.com/en/1.7/_modules/django/contrib/auth/forms/#AuthenticationForm

    Pass this into the login form via the `authentication_form` parameter.
    - https://docs.djangoproject.com/en/1.7/topics/auth/default/#django.contrib.auth.views.login
    Which is done in `registration/urls.py`.
    """
    username = forms.CharField(min_length=USERNAME_MIN_LEN,
                               max_length=USERNAME_MAX_LEN,
                               error_messages={
                                   'min_length': username_min_max_len_err_msg,
                                   'max_length': username_min_max_len_err_msg })
    password = forms.CharField(label=_("Password"), widget=forms.PasswordInput,
                                    min_length=PASSWORD_MIN_LEN,
                                    max_length=PASSWORD_MAX_LEN,
                                    error_messages={
                                        'min_length': pwd_min_max_len_err_msg,
                                        'max_length': pwd_min_max_len_err_msg })
#    def clean(self):
#        raise ValidationError("Yikes")
Community
  • 1
  • 1
aliteralmind
  • 19,847
  • 17
  • 77
  • 108
  • What does the login_maybe_remember view look like? – Daniel Roseman Feb 23 '15 at 19:18
  • @DanielRoseman: Added the entire file to the bottom. – aliteralmind Feb 23 '15 at 19:24
  • Hmm. Can't see anything obviously wrong there. Have you confirmed that you're hitting the right view, and it's correctly passing the form parameter? – Daniel Roseman Feb 23 '15 at 20:40
  • @DanielRoseman: Added `print("authentication_form=" + str(kwargs["authentication_form"]));` Immediately before the return, in the login view, and it prints `authentication_form=` on every login attempt (successful or not). – aliteralmind Feb 23 '15 at 21:51
  • I'm pretty stuck. Django is installed remotely on Digital Ocean, on a command-line only Ubuntu box (no GUI). I would love to install Vagrant on my local Windows machine, so I can activate some debugging, as was suggested to me on irc/django, but both Vagrant and Python debugging in general--not to mention Django debugging--are unfamiliar to me. I am required to switch over to a Java project tomorrow and unfortunately don't have the time to dive into something so new. :( Any ideas would be much appreciated. (I must say it's a compliment that you don't see anything wrong with my code.) – aliteralmind Feb 23 '15 at 21:56
  • FYI, this is to become (an updated version of) [chapter six](https://aliteralmind.wordpress.com/2015/02/12/django_auth_tutorial_6/) in my [auth tutorial](https://aliteralmind.wordpress.com/2014/10/16/django_auth_tutorial/). – aliteralmind Feb 23 '15 at 21:58
  • In the meantime, the good news is that this is only a problem if client-side JavaScript is turned off/fails. I'll document that clearly. – aliteralmind Feb 23 '15 at 23:08

0 Answers0