Code


Version 1 (modified by jkocherhans, 8 years ago) (diff)

initial and very rough revision of multiple auth backends

Multiple Authentication Backends

Currently, Django's authentication system assumes that all your users live in the db as Django models and you must use a username and password to login. This works ok until you realize that you need to support 200 users who already have accounts Active Directory (or insert your favorite windows-free LDAP server here.) In short, Django shouldn't require you to use Django models for users, and it shouldn't require you to use a username and password. What if I want to use an email address, or use some other unique key for my boss REST interface?

Zope has a pretty simple/cool solution for this. It's not really standalone, but it's not that hard to port either. Regardless of what you think of Zope, they've been doing this longer than most of us, and we could probably learn a thing or two from them. Most (meaning nearly all) of these ideas are modeled on zope.app.authentication

The details are a little flaky, and the naming kinda sucks, please make suggestions. Basically, here's how it goes:

Credentials

Instead of hard-coding checks for a cookie, or a username/password etc. etc. let's let different callables just grab the credentials from the request. In fact, let's put a bunch of these plugins in a list, so if the first one fails, we can try another.

class UsernamePasswordPlugin:
    def extract_credentials(self, request):
        return {'username': request.POST['username'], 'password': request.POST['password']}

We can add plugins for extracting credentials from a sessions, getting an api key, whatever, and configure them like so:

Add this to settings.py:

CREDENTIALS_PLUGINS = (
    'django.contrib.auth.SessionCredentials',
    'django.contrib.auth.UsernamePasswordCredentials',
)

Authentication

Credendials only get us half-way there. We'd better check them against something. How about a list of authentication backends:

Add this to settings.py:

AUTHENTICATION_PLUGINS = (
    'django.contrib.auth.ModelAuthenticator',
    'django.contrib.auth.LDAPAuthenticator',
)

And now for the plugin:

from django.contrib.auth.models import User

class ModelAuthenticator
    def authenticate_credentials(self, credentials):
        #we need a username and password in the credentials, if they're not there, return None
        #keep in mind that other authenticators can look for different types of credentials
        #if the username or password or bad, return None
        #if everything is kosher, return the USER

So Zope uses a factory to create a user. Your auth plugin can do whatever you feel like as long as it returns a user object. (We need to come up with a minimum informal interface for a user object. Use duck typing, not formal interfaces. The user does not need to be a django model object, but it can be. Read the last sentence again.) So if we're writing an LDAP plugin, we could make one that actually creates a Django model the first time an LDAP user successfully logs in. We could use events/factories to make this even more abstract and flexible. Let's lay the groundwork first though.

Multiple Backends

Um, so what if I want to use LDAP and Django models? Funny you should ask.

class AuthenticationUtility:
    def authenticate(self, request):
        #psuedocode ahead!
        for cred_plugin in credentials_plugins:
             credentials = cred_plugin.extract_credentials(request)
             if credentials is not None:
                 for auth_plugin in auth plugins:
                     user = auth_plugin.authenticate_credentials(credentials)
                     if user is not None:
                         return user

This is the main interface to the auth system. In english, check for credentials, if we find them, try to authenticate them against every auth backend in settings.py, if none of them authenticate, do it all again with the next type of credentials.

Credentials plugins should probably have a logout method. I didn't give any examples, but it shouldn't be hard.

That's most of it. Think you have a use case this won't work for? Please tell django-developers.