Code


Version 3 (modified by Max Battcher <me@…>, 8 years ago) (diff)

Suggestions

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. This will cause problems for permissions and groups. Eventually, those should be reworked so they aren't required to be django models either.) 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, or we could just assemble and return a plain old python object. 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.

Suggestions

It makes a lot of sense to store a cache of a user object as a django model, no matter what the authentication scheme, so it might very well be useful to just go ahead, reduce the Users models to its barest core, and I would add an (optional) Expiration date for automatic cache/culling (ie, always check the db first, then try everything else). (It may even be useful to add Expiration dates to permissions/groups.) I'm also thinking that many of the Credential types should just all be models in their own right with a Many-to-Many relationship with the base User class (a user could be very likely to have multiple Credentials, and there might be the occaisional need for "Group Credentials"). Once the subclassing system is in place, all of the Credential models should be subclasses of each other, probably, for good Pythonic OO-ness. Probably most DB Credentials would want Expiration dates, too. This would be a useful generic/easy system for those GET confirmation Credentials that people do. Finally, keep in mind that not all Credentials will have a username/password combo. GET Credentials will probably be some random string or hash. OpenID Credentials are URLs (tied to remote server response signatures and remote server spam white/blacklists). Making Credentials first-class models would help make it easier to remove most of the cases where a seperate User model might be necessary (ie, OpenID users could share the same base User model that Django users do, and someone can use the very same base model if they happen to use both). --Max Battcher