Django Authentication using an Email Address

The Django authentication system provided in django.contrib.auth requires the end user to authenticate themselves using a username and password. However, it is often desireable to allow users to log in using an email address rather than a username. There are a few different ways to accomplish this and a lot of discussion on the topic.

In this approach, I use a custom authentication backend to authenticate a user based on his or her email address, I hide the username from the end user, I generate a random username for the user when creating an account, and I manually add a unique index to the email column in the database (this is a bit of a "hack" and I'd love to hear suggestions in the comments).

Why email authentication?

The web has evolved and these days many of us have dozens (hundreds?) of accounts around the web. Remembering all those usernames and passwords can be a real chore. That "forgot password" feature suddenly becomes a standard step in the log in process for many users. When ease-of-use is paramount in your authentication system, such as customer accounts on e-commerce websites, you want to require as little effort from the end user as possible. Most people already remember their email address. One less piece of information to remember.

Why not email authentication?

Yes, this post is about using email authentication, but it has it's down side as well. We know users forget their passwords quite often. Users can also lose their email address when they change jobs, schools, or their domain name expires. If a user has lost access to the email they used in your system and they do not remember their password then there is nothing they can do short of starting a new account on your site (unless you implement something to specifically address this scenario). For some applications this is an acceptable trade-off for the usability gained by using email authentication.

Problems Presented with Email Authentication in Django

The email address is not unique.

The Django authentication system uses the a username to uniquely identify a user. While this is often used as a "display name" as well, it's primary function is as a unique identifier for a user. In theory, Django's auth system could allow two users to share the same email address and thus, the email address does not uniquely identify a user.

There is no database index on the email address.

If the authentication system is going to be querying users on their email address, the database column storing that email address would need to have an index. The datatbase table created by the Django authentication, auth_user, does not have any index on the email address, let alone a unique index.

The username is limited to 30 characters.

Some people implement email authentication in Django by putting the email address into the username field. The problem with this is that the username field is limited to 30 characters and many (about 15% on one of my websites) are longer than 30 characters.

Other code relies on django.contrib.auth.

There are a lot of other Django apps, both built-in and 3rd party, that make use of Django's authentication system. Thus, it is ideal to work with Django's authentication system opposed to "rolling your own" system.

A Custom Backend Solution for Email Authentication in Django

The approach I have opted for begins with a custom authentication backend. The code below defines the backend that django.contrib.auth will use instead of the default backend. The username passed to the authenticate() method is assumed to be an email address and the user is queried based on the email instead of the username.

backends.py
from django.contrib.auth.models import User, check_password

class EmailAuthBackend(object):
    """
    Email Authentication Backend
    
    Allows a user to sign in using an email/password pair rather than
    a username/password pair.
    """
    
    def authenticate(self, username=None, password=None):
        """ Authenticate a user based on email address as the user name. """
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
        except User.DoesNotExist:
            return None 

    def get_user(self, user_id):
        """ Get a User object from the user_id. """
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

You can then use the AUTHENTICATION_BACKENDS setting in your settings.py file to tell the django authentication system to use your custom backend.

settings.py
AUTHENTICATION_BACKENDS = ('backends.EmailAuthBackend',)

Renaming the "Username" Field to "Email address"

Since the custom authorization backend assumes the username passed to it is an email address, the standard authentication form works just fine. The user just enters an email address instead of a username. However, the form still has the username field and that field is still labeled "Username".

Since I prefer more granular control over the layout of the forms in the authentication process, I opted to let the login form template handle changing the label of the authentication form. An alternative approach would have been to subclass django.contrib.auth.forms.AuthenticationForm and rename the field's label there.

templates/login_form.html (example snippet)
<form action="." method="post">
  {% csrf_token %}
  <input type="hidden" name="next" value="{{ next }}" />
  {{ form.non_field_errors }}
  {% for field in form %}
    <div class="field-wrapper">
      <div class="label-wrapper">
        {% if field.name == "username" %}
          Email address
        {% else %}
          {{ field.label_tag }}
        {% endif %}
        {% if field.field.required %}<span class="required">*</span>{% endif %}
      </div>
      <div class="value-wrapper">
        {{ field }}
        {{ field.errors }}
      </div>
    </div>
  {% endfor %}
  <div class="submit-wrapper">
    <input type="submit" value="Sign In" />
  </div>
</form>

A Custom Sign Up Form (User Creation Form)

Since an email address is now required to authenticate a user, it needs to be a required field on the user creation form as well. This can be done by subclassing the UserCreationForm.

forms.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django import forms

class SignUpForm(UserCreationForm):
    """ Require email address when a user signs up """
    email = forms.EmailField(label='Email address', max_length=75)
    
    class Meta:
        model = User
        fields = ('username', 'email',) 

    def clean_email(self):
        email = self.cleaned_data["email"]
        try:
            user = User.objects.get(email=email)
            raise forms.ValidationError("This email address already exists. Did you forget your password?")
        except User.DoesNotExist:
            return email
        
    def save(self, commit=True):
        user = super(UserCreationForm, self).save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        user.email = self.cleaned_data["email"]
        user.is_active = True # change to false if using email activation
        if commit:
            user.save()
            
        return user

Now the email address is added to the form, however, the username is still a required field as well. That may suit your needs, however, my goal is to keep the process as simple as possible for the end user. I do not want to remove the username, I just want to hide it from the end user. So I simply hide the username field in the template for the form and then auto-generate a random username in the view.

templates/sign_up.html (example snippet)
<form action="." method="post">
  {% csrf_token %}
  {{ form.non_field_errors }}
  {% for field in form %}
    {% if field.name != "username" %}
      <div class="field-wrapper">
        <div class="label-wrapper">
          {{ field.label_tag }}
          {% if field.field.required %}<span class="required">*</span>{% endif %}
        </div>
        <div class="value-wrapper">
          {{ field }}
          {{ field.errors }}
        </div>
      </div>
    {% endif %}
  {% endfor %}
  <div class="submit-wrapper">
    <input type="submit" value="Sign Up"/>
  </div>
</form>

Generating a Random Username in the View

Since the username field was omitted from the sign up form template, the view that processes the form needs to create one. The example view below generates a random 30-character username.

from django.template import RequestContext
from django.shortcuts import render_to_response
from django.http import HttpResponseRedirect
from forms import SignUpForm
from random import choice
from string import letters

def sign_up(request):
    """ User sign up form """
    if request.method == 'POST':
        data = request.POST.copy() # so we can manipulate data

        # random username
        data['username'] = ''.join([choice(letters) for i in xrange(30)])
        form = SignUpForm(data)
            
        if form.is_valid():
            user = form.save()
            return HttpResponseRedirect('/sign_up_success.html')
    else:
        form = SignUpForm()

    return render_to_response('sign_up.html', {'form':form},
                              context_instance=RequestContext(request))

Adding a Unique Index to the Email Column

The only part of this email authentication solution that I really do not like is that I have am manually adding a unique index to the email address column in the database (I use MySQL). Please post a comment with your suggesions for a better solution.

I know of some people who use the email address in a custom profile, however I don't like the redundancy nor having the primary unique identifier for a user in a separate table. The ideal solution for me would be to have some code that can unobtrusively add a unique index to the auth_user table through Django's database abstraction.

SQL
ALTER TABLE auth_user ADD UNIQUE INDEX (email);
Did you enjoy Django Authentication using an Email Address? If you would like to help support my work, A donation of a buck or two would be very much appreciated.
blog comments powered by Disqus
Linux Servers on the Cloud IN MINUTES