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.
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.
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.