Custom authentication in Django
The Django administration system is very useful for small teams who work closely together, but not very useful for widely separate individuals who each work on their own records but shouldn’t work on other people’s records.
I’m currently working on a simple CMS, and when I and one other person were working on the web pages in it, the Django admin worked great; when we start adding other people, it won’t be as useful. We’ll need to create our own access limits on a per-record basis.
You probably already know how to limit access to a view to logged-in Django administrative users; add the @login_required decorator above the view. But login_required is just a Python function. We can put whatever function we want there.
Here’s an example of what we might want an authentication function to do. First, block access to the view unless the user logs in and the user has access rights to that record; and second, store the user in the session for future reference.
Here are two views, one of which shows all of the roles for that user, and one of which shows a form for editing a specific record:
[toggle code]
- from django.shortcuts import get_object_or_404, render_to_response
- from spacepages.pages.models import Page
- import spacepages.cms.login as login
- @login.authenticate
-
def showroles(request):
- user = request.session['person']
- roles = user.roles()
- pageContext = {'roles': roles, 'user': user}
- return render_to_response("cms/pageindex.html", pageContext)
- @login.authenticate
-
def editpage(request, page_slug):
- user = request.session['person']
- page = get_object_or_404(Page, slug=page_slug)
- pageForm = CMSForm(page=page, request=request)
- pageContext = {'form': pageForm, 'page': page, 'user': user}
- return render_to_response("cms/editpage.html", pageContext)
This makes for a very simple views.py: two functions, and each function displays a view. The important part here is that we restrict access to those functions according to the “authenticate” function in the imported “login” file. There also has to be a form object somewhere, here called CMSForm. That would be specific to your records.
Decorators
What Python does when it sees a decorator in front of a function is, it first calls the function specified by that decorator. Before Python calls “showroles” or “editpage”, it will call “login.authenticate”. It will call the decorator function with one parameter: the function that otherwise would have been called.
The decorator function is in full control. So, for example, if we want to block access to “editpage” by this browser, we could return a rendered page asking them to log in (or telling them that they don’t have access for whatever reason).
If the decorator function returns a function (remember that Python can handle functions just as it can handle strings, numbers, or booleans), then Python calls that function with the parameters of the original decorated function. In this case, for example, “editpage” is called with the request and the page slug. If login.authenticate returns a function—and it can be any function, not just editpage—then that function will be called with those parameters.
So here’s what we can do. Inside of login.py, we can set up an authentication object and then return a method on that object:
[toggle code]
-
def authenticate(view):
- verifier = authenticator(view)
- return verifier.authenticate
When Python calls this authenticate function, it will call it with either “showroles” or “editpage”. The function then creates an authenticator object to remember that, and returns the authenticate method on the new authenticator object.
Python will then call that method with either the request (for showroles) or the request and the page slug (for editpage).
The authenticator
The authenticator object needs to remember the original decorated function when it gets initialized, and then it needs to verify that there is a valid login. If there isn’t a valid login, it shows the login form. If there is a valid login, it returns control to the original decorated view.
Here’s a simple form:
[toggle code]
-
<html>
-
<head>
- <title>Login Required</title>
- </head>
-
<body>
- <h1>Login Required</h1>
-
<form action="{{ action_url }}" method="post">
- <h2>Please log in:</h2>
- <table>{{ form }}</table>
- <input type="submit" value="Log In" />
- </form>
- </body>
-
<head>
- </html>
The authenticator will need to provide the action_url and the form in the context.
[toggle code]
- from django.shortcuts import render_to_response
- from django import newforms as forms
- from django.core.exceptions import ObjectDoesNotExist
- from spacepages.pages.models import Page
- from spacepages.cms.models import Person
-
class LoginForm(forms.Form):
- user = forms.CharField(max_length=40)
- password = forms.CharField(max_length=40, widget=forms.PasswordInput)
-
class authenticator:
-
def __init__(self, method):
- self.view = method
-
def authenticate(self, request, page_slug=None):
- self.request = request
- form = None
- person = None
-
if 'person' in request.session:
- person = request.session['person']
-
elif request.POST:
- form = LoginForm(request.POST)
-
if form.is_valid():
- person = self.verifyAccount(form['user'].data, form['password'].data):
- request.session['person'] = person
-
if person:
-
if page_slug:
-
if person.has_access(page_slug):
- return self.view(request, page_slug)
-
if person.has_access(page_slug):
-
else:
- return self.view(request)
-
if page_slug:
-
if not form:
- form = LoginForm()
- action = 'http://' + request.META['HTTP_HOST'] + request.META['PATH_INFO']
- context = {'form': form, 'action_url': action}
- return render_to_response("cms/login.html", context)
-
def verifyAccount(self, username, password):
-
-
try:
- person = Person.objects.get(username=username)
-
except ObjectDoesNotExist:
- return False
-
if person.verify(password):
- return person
-
try:
- return False
-
-
def __init__(self, method):
There are three methods. The __init__ method remembers the decorated view; the authenticate method checks for an existing session or a form submission, and the verifyAccount method takes a username and a password and gets the corresponding Person object.
If there’s already a valid session or if the person logs in and has access to this page, it calls the originally decorated view.
Determining whether the password is correct is handed off to the Person object, as is determining if this person has access to the page. You’ll need to create a Person model with appropriate information for your project, such as a username or e-mail address, a password (stored hashed, of course), and other information such as full name, web site, etc. Also, I don’t know how secure Django’s session system is. You might want to add a timeout or some sort of a one-time key.
- January 21, 2008: Remembering Django form submissions when authenticating
-
If you play around with the built-in Django login mechanism, you might notice that it has one very useful feature: if your session ends before you submit a form, it will save your form data while you log in, and it lets you know this so that you don’t panic.
Django does this by saving the actual POST dictionary into the form. It encodes it using a special function that pickles it and turns it into an ASCII representation that can be stored in a form.
If we take the line “return render_to_response("cms/login.html", context)” from the authenticate method on the authenticator object and replace it with “return self.login_form(context)”, we can make a smarter login form.
[toggle code]
- from django.contrib.admin.views.decorators import _encode_post_data
- …
-
def login_form(self, context):
- request = self.request
-
if request.POST and 'post_data' in request.POST:
- # User has failed login BUT has previously saved post data.
- post_data = request.POST['post_data']
-
elif request.POST and not 'login_submission' in request.POST:
- # User's session must have expired; save their post data.
- post_data = _encode_post_data(request.POST)
-
else:
- post_data = None
- context['post_data'] = post_data
- return render_to_response("cms/login.html", context)
There are three cases to check for here: the person is just logging in; the person is logging in after attempting to submit a form; and the person is logging in after mistyping their username or password after attempting to submit a form. And we need to be able to tell the difference between submitting a form that has data worth keeping, and submitting the login form.
For the latter, I added a hidden “login_submission” field to the form. So the logic is this:
- If there is POST data and there is a “post_data” field, keep it.
- If there is POST data and there is no “login_submission” field, save the POST data in post_data.
- Otherwise, don’t save any POST data.
- Django
- “Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.” Oh, the sweet smell of pragmatism.
More Django
- Django: fix_ampersands and abbreviations
- The fix_ampersands filter will miss some cases where ampersands need to be replaced.
- Custom managers for Django ForeignKeys
- I’ve got one really annoying model for keywords. There’s one category of keywords that, by default, should not show up when used as a ForeignKey for most models. Key word: most.
- Fixing Django 1.2.4’s SuspiciousOperation on filtering
- When you get the message “Filtering by keyword not allowed” in Django 1.2.4, here’s one way to fix it.
- Reusing Django’s filter_horizontal
- Just as with pop-ups, it’s possible to use the built-in JavaScript for filtering multiple-selection popups on custom forms.
- Django formsets and date/time fields
- Date/Time fields in Django formsets appear to have incompatible default values, resulting in forms using them always looking as though they’ve got a new entry when they don’t.
- 25 more pages with the topic Django, and other related pages
More Python
- Parsing JSKit/Echo XML comments files
- While I’m not a big fan of remote comment systems for privacy reasons, I was willing to use JSKit as a temporary solution because they provide an easy XML dump of posted comments. This weekend, I finally moved my main blog to custom comments; here’s how I parsed JSKit’s XML file.
- Put a relative clock on your Desktop with GeekTool
- There are a lot of desktop clocks that show the absolute time. But sometimes you just want to know if the time is today, or yesterday, or two days ago. Here’s how to do it with Python and GeekTool.
- Multiple tables on the same command
- The way the “random” script currently stands, it does one table at a time. Often, however, you have more than one table you know you’re going to need. Why not use one command to rule them all?
- Easier random tables
- Rather than having to type --table and --count, why not just type the table name and an optional count number?
- Programming for Gamers: Choosing a random item
- If you can understand a roleplaying game’s rules, you can understand programming. Programming is a lot easier.
- 24 more pages with the topic Python, and other related pages
