roll your own django authentication system February 27, 2008

Django comes with batteries included - the authenticated system is just another application provided by the framework. Use it and you'll have access to a lot of free code that's based off of it (like Django's famous automatic admin interface).

Of course, there are benefits when you write your own authentication code:

  • you get to define the User model specifically tailored for your application
  • you can make a lightweight solution

To roll your own authentication system, you'll need to implement these parts:

  • storing username/password in your User model
  • have view functions to log your user in/out
  • protect your view functions that need authentication

the user model

We'll start with the User model which holds the username and password. If your application needs any more fields (like first_name and last_name), feel free to add those:

from django.db import models

class User(models.Model):
    username = models.CharField(maxlength=64)
    crypted_password = models.CharField(maxlength=40)
    salt = models.CharField(maxlength=40)

Instead of saving the password as a plain text field, we're going to hash it using SHA1. If our servers ever get stolen, our users' passwords won't be compromised because they're encrypted.

Python's hashlib module provides the SHA1 code for us. It's installed by default for Python2.5, for 2.3 and 2.4 you'll need to install it manually.

import hashlib
import datetime

class User(models.Model):
    # after your database fields, add these methods

    def __encrypt(self, plaintext, salt=""):
        """returns the SHA1 hexdigest of a plaintext and salt"""
        phrase = hashlib.sha1()
        phrase.update("%s--%s" % (plaintext, salt))
        return phrase.hexdigest()

    def set_password(self, new_password):
        """sets the user's crypted_password"""
        if not self.salt:
            self.salt = self.__encrypt(str(datetime.datetime.now()))
        self.crypted_password = self.__encrypt(new_password, self.salt)

    def check_password(self, plaintext):
        return self._encrypt(plaintext, self.salt) == self.crypted_password

The __encrypt private method returns the result of a hashed string/salt. Salts are added measures of security to prevents dictionary attacks. Our set_password method sets the user's crypted_password field for us, taking in a plaintext password and encrypting it for us.

check_password works by encrypting a plaintext string and making sure it matches the crypted_password column. We'll use check_password to make sure a visitor enters the correct password.

authenticating a user via UserManager

We'll subclass a Django manager to add an authenticate method. It checks to see if a username/password combination is correct:

class UserManager(models.Manager):
    def authenticate(self, username, password):
        user = User.objects.get(username=username)
        if user.check_password(password):
            return user
        else:
            raise User.DoesNotExist

class User(models.Model):
    # fields/methods above go here
    objects = UserManager()

logging a user in/out

Django sessions stores semi-persistent data about a website visitor which is especially handy for authentication. In this case, it's going to store the user's id after he or she logs in.

# in your views.py
def login(request):
    username = request.POST['username']
    password = request.POST['password']
    try:
        user = User.objects.authenticate(username, password)
        request.session['user_id'] = user.id
        return HttpResponseRedirect('/admin/')
    except User.DoesNotExist:
        return render_to_response('login.html', {username: request.POST['username'], 
                              {msg: 'Incorrect username/password combination'})

If the visitor has the correct username/password, we'll log him in by setting session['user_id'] to the user's id. Otherwise, we'll re-render the login screen with an error message.

Now we know a visitor is authenticated if request.session['user_id'] is set. To logout a user, just clear the session variable:

def logout(request):
    request.session['user_id'] = None
    return HttpResponseRedirect('/login/')

protecting your view functions that require authentication

For all of our view functions that need authentication, we need to check if the session['user_id'] is set and corresponds to an actual User. This gets repetitive if you do it in every function. We could use Middleware to protect our views but I'm going to use a simple decorator. Decorators are just functions that take a function as an argument and returns a modified one.

def login_required(func):
    def _decorator(request, *args, **kwargs):
        try:
            request.user = User.objects.get(id=request.session['user_id'])
            return func(request, *args, **kwargs)
        except (KeyError, User.DoesNotExist):
            request.session['user_id'] = None
            return HttpResponseRedirect('/login/')
    return _decorator

This decorator just will set request.user to the current logged in user according to the session's user_id. If none is found, it will redirect the user to the login URL. Python decorators can be used like this:

def secret_view(request):
    # this view function should only be accessed by users that are logged in
    # we can access the currently logged in user from `request.user`
secret_view = login_required(secret_view)

If you're using Python 2.5 or above, you can use the special decorator syntax:

@login_required
def secret_view(request):
    # this view function is protected

roll your own or use Django's built-in authentication?

For projects that use Django's automatic admin interface, you'll need to use Django's built-in auth. For other projects, decide whether or not you need a more custom system or if Django's generic authentication will fit your needs.

Once you implement your own system, it's easy to package it up as an application and re-use it in your other projects.

← see recent posts | comments closed 6 comment(s)

Kyle 02/28/08 09:36:50 PM #1

First, setting the user ID in a cookie is very insecure. It's dead simple for anyone to send a fake cookie with another user's ID an "be authenticated" as them. You should probably be creating some kind of hash.

We've done a lot of django stuff and never once had to roll our own entire User model with an authentication framework.

We've been able to meet almost any requirement by simply using the AUTH_USER_PROFILE setting, or by writing our own authentication backend.

Thanks for the write-up! You should take a look at the authentication system in django to see a few of the best practices at work.

Hugh Bien 02/28/08 10:20:50 PM #2

Thanks Kyle!

I definitely agree that storing the user's ID in a cookie is insecure. But this example stores it in the sessions hash which can only be accessed by the server side, so it should be okay.

AUTH_USER_PROFILE and authentication backends both look like great ways to extend the existing auth system in Django, I'll look into them.

Vire7 03/19/08 01:48:12 AM #3

This is great and perfect timing. Muchas gracias!!!

moos3 04/05/08 03:05:03 PM #4

This is great, just what I needed for my photo gallery.

chris 04/17/08 01:21:48 PM #5

Nice article but why would you wanna write your own system?

1) It's a waste of time when one is already there. 2) You can create different auth backends as you mentioned which is all you really ever need.

  • you get to define the User model specifically tailored for your application
  • you can make a lightweight solution

Why do this when you can extend your user model to tailor to your app's needs. I don't think that I would want to get much more lightweight than the already existing auth system. If at some point there is a flaw with the auth system you have the satisfaction that it will get fixed whereas a custom solution your on your own. What fate would you chose?

Hugh Bien 04/17/08 08:30:01 PM #6

@Vire7 and moos3, thanks!

@chris, thanks for the input. Sometimes a pre-built solution doesn't always cut it. I usually use the Django's built in auth system but I've also rolled my own to integrate license keys.

about me

My name is Hugh, I code in Obj-C & Python.

latest posts

related links

other sites

subscribe with rss

(c) Copyright 2007-2008 Hugh Bien. All Rights Reserved.

Contact Me | RSS Feed