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
:
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")