Wednesday, November 26, 2014

Custom Django Middleware

I needed to require login for every view within a Django project. This project had multiple apps and multiple views within those apps. Sprinkling the @login_required decorator around every view was tedious, and didn't seem like a clean solution.

After doing a couple of of searches, I came across a few, older (2010/2011) middleware packages that looked like good solutions. Since the packages were older, and I have never written any custom middleware before for Django, I decided to write my own solution. I was really surprised at how easy it was to plug in some custom middleware.

Here's how I created my own middleware to require login for every view within my project.

Create a "middleware.py" for your project


I placed this in my project configuration folder. It doesn't matter where you put the file, as long as you reference the path to it properly in your settings file (more on this later).

Create your custom Middleware Component


Per Django's documentation: Each middleware component is a single Python class. So, creating your own middleware component looks like this:

class MyMiddlewareClass(Object):
  # define custom methods in here

For my purposes, I created the following class:

class LoginRequiredMiddleware(Object):
  # my custom methods in here

Build out your custom Middleware Methods


I needed to check if the user was authenticated or not, before processing the view. So, I defined the process_view method.

from django.http import HttpResponseForbidden

  class LoginRequiredMiddleware(object):

  def process_view(self, request, view_func, view_args, view_kwargs):
    if request.user.is_authenticated():
      return None
    else:
      return HttpResponseForbidden()


This is a very basic implementation of the functionality I needed. Before Django calls the view (view_func), this method simply checks if the request is authenticated or not.

If it is, it returns None, which tells Django to continue on processing the request as per usual, eventually executing view_func. If it isn't, it returns the convenient HttpResponseForbidden.

I defined the process_view method as opposed to process_request method because I wanted Django to catch 404 errors before running my custom middleware. Otherwise, my middleware would send an unauthenticated user to the login page even when they requested a URL that hasn't been defined in my URL patterns.

Obviously this is too basic. What about the STATIC_URL and MEDIA_URL? Or the Django Admin, or any other pages you want to be public?

To handle these contingencies, I built out the method a bit more to look like this:

import re

from django.http import HttpResponseRedirect
from django.conf import settings

class LoginRequiredMiddleware(object):

  PUBLIC_URL_PATTERNS = [
    r'^%s.*$' % settings.STATIC_URL,
    r'^%s.*$' % settings.MEDIA_URL,
    r'^/admin/.*$',
    r'^/(\?.*)?$',
    r'^%s.*$' % settings.LOGIN_URL,
  ]

  PUBLIC_URL_PATTERNS = [re.compile(exp) for exp in
                        PUBLIC_URL_PATTERNS]

  def process_view(self, request, view_func, view_args, view_kwargs):
    path = request.path
    if any(pattern.match(path) for pattern in 
      self.PUBLIC_URL_PATTERNS):
      return None

    elif request.user.is_authenticated():
      return None

    else:
      redirecturl = settings.LOGIN_URL + "?go=" + path
      return HttpResponseRedirect(redirecturl)


The method now checks to see if the requested URL matches any public URL defined in PUBLIC_URL_PATTERNS. It also appends a query string to the LOGIN_URL that the user is re-directed to. So that we can easily send the user to the URL they requested after he/she logs in.

This is still pretty basic. And it only accounts for a few public URL patterns. It'll need to be built out a bit to be more robust. And I might follow up with another post once I do that. I wouldn't recommended blindly copying this code for use in your own project.

Add your custom Middleware to your settings


MIDDLEWARE_CLASSES = (
    ...,
    'projectroot.middleware.LoginRequiredMiddleware',
)

Be sure to replace projectroot with the module name of your project configuration folder. My full path, for example, is "dylansproject.middleware.LoginRequiredMiddleware". Now your project is all set up to use your custom middleware!