Django 1.8 Tutorial – 5.1 Alternative Authentication (Make Your Own)

I was hoping to get into Allauth, or maybe that and some more UX tweaks, but ended up hitting some walls. We’re trying to implement an authenticator that works with an external service, but it’s more complex than expected. (I won’t be posting it here.)

To get to the point where I could even write parts of the authenticator, I needed to teach myself how to write authenticators. So, here are two authenticators. Both are a little odd, but I think the code is short enough that it won’t be confusing.

If it gets confusing because I’m using a couple libraries – study the libraries. If you’re not already familiar with subprocess and requests, plan to spend a few more hours to learn those great libraries. It’s worth the effort.

The two authenticators are auth_command and auth_proxy_http. Auth_command executes an external command, and allows the login if it passes. Auth_proxy_http makes a request to another URL; the URL must be set up to accept an HTTP-Authorization username and password. If it returns a status code of 200 to 299, it’s considered authorized.

To make auth_command, you use

./manage.py startapp auth_command

Then, in that directory, create this command in the file “command”:


#! /usr/bin/python
"""
  Usage: command username password
  Returns 0 on success, 1 on failure.
"""
import sys

passwd = {
    'paul': 'password',
}

try:
    username = sys.argv[1]
    password = sys.argv[2]
except:
    sys.exit(1)

try:
    if passwd[username] == password:
        sys.exit(0)
    else:
        sys.exit(1)
except Exception as e:
    sys.exit(1)

This is a simple unix command that checks the passwd dictionary to look up a password.

Then, make a file, auth.py:


from django.contrib.auth.models import User
import subprocess, inspect, os


class AuthCommandBackend(object):
    def authenticate(self, username=None, password=None):
        try:
            command = os.path.dirname(inspect.getfile(
                inspect.currentframe()))
            command = '%s/command' % (command,)
            # print 'running command %s' % (command,)
            # run the command
            exitcode = subprocess.call(
                [command, username, password])
            if exitcode == 1:
                return None
        except:
            return None

        user, created = User.objects.get_or_create(username=username)
        if (created):
            user.set_password(password)
        return user

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

It checks that the command accepts the username and password. If it doesn’t it returns None. Otherwise, it proceeds to try and get a Django user with the same username, creating it if it doesn’t exist.

It also sets the password. This means that future logins are performed against the User, not this other command. If you want the command to always verify the password, don’t set a password on the created model. Then, the login for the User will always fail, and fall back to using the command. (Think about this a bit before deciding what to do.)

To use this authentication app, you add the auth module to the AUTHENTICATION_BACKENDS in the settings:


AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend', # default
    'auth_command.auth.AuthCommandBackend',
)

Once this is done, you can try to log into the comment section with the username “paul” and the password “password”.

Auth_proxy_http is a similar authentication, but uses the network.

It uses the Requests library, so install it with:

pip install requests

Do a

./manage.py startapp auth_proxy_http

then create auth.py:


from django.contrib.auth.models import User
import requests


class AuthProxyHttpBackend(object):
    url = 'http://localhost/protected/'

    def authenticate(self, username=None, password=None):
        try:
            r = requests.get(self.url, auth=(username, password))
            if r.status_code < 200 or r.status_code >= 300:
                return None
        except:
            return None

        user, created = User.objects.get_or_create(username=username)
        if (created):
            user.set_password(password)
        return user

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

Set the URL to whatever you want. We should really check a global for a URL value, and then add some notes about overriding this class to specify a different URL.

The code is similar to the command authenticator, except it checks http://localhost/protected/, which, presumably, is protected with a password in the HTTP-Auth style. This is the classic .htaccess file technique.

You would create a file in the website’s /protected/ directory called .htaccess, with this content:


AuthType Basic
AuthName "restricted area"
AuthUserFile /home/johnk/Sites/riceball/htpasswd
require valid-user

Then, you create the file specified in AuthUserFile, like this:

htpasswd -c /home/johnk/Sites/riceball/htpasswd test

Then enter the password.

Then, add to your settings:


AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend', # default
    'auth_command.auth.AuthProxyHttpBackend',
)

Now, logins to your Django app will be checked against that URL. If you add someone to the htpasswd, they’ll also gain access to the Django site.