Code


Version 13 (modified by Chris Isbell, 8 years ago) (diff)

I got LDAP auth working using the multi-auth branch, which is nice. Copied contrib.auth.backends and added a few things to contrib.auth.models.

Multiple Authentication Backends

See http://code.djangoproject.com/ticket/1428 for the current patch that implements this.

Authentication

The default authentication mechanism for Django will still check a username/password against django.contrib.auth.models.User

The path to the default authentication backend can be set in settings.py via the AUTHENTICATION_BACKEND variable. This backend is used to set the request.user attribute automatically. There is also a backend that is just a front for using multiple backends, but we'll get to that later.

Authenticating

Here's a code sample that authenticates a user. This would be used to process login forms. Like before, you'd check request.user.is_anonymous() if you want to test if user is logged in.

from django.contrib.auth import AuthUtil

def login(self, request):
    authutil = AuthUtil()
    user = authutil.authenticate(request)
    if user is None:
        # do whatever for invalid logins
    else:
        # the user is valid, persist their id (username, email, token, etc.) in a session var or whatever.
        # do whatever else this view is supposed to do.

Note that the view is in charge of "logging in" a user, that is, the view must persist the id of the user somehow. A session variable is the easiest place, but a signed cookie would be desireable for those who don't want to use the session middleware. It would be nice if this were more convenient. It shouldn't be hard to do. Also, in practice, all of this would probably happen in a decorator. Adding that much boilerplate code to every view would be a little ridiculous.

For extra points, there should be ways of tying this all in with WSGI ;)

Credentials

Credentials are extracted from the request by plugins. These plugins are just functions that take the request as their only argument and return a dict or string containing the credentials. You can have multiple ordered credential plugins by changing CREDENTIAL_PLUGINS in your settings file.

CREDENTIAL_PLUGINS = (
    'django.contrib.auth.credentials.username_password_form',
    'django.contrib.auth.credentials.token',
)

AuthUtil will use the first plugin and hand the credentials to AUTHENTICATION_BACKEND. If AUTHENTICATION_BACKEND returns None for the first set of credentials, the next plugin will be tried, and so on.

CREDENTIAL_PLUGINS defaults to ('django.contrib.auth.credentials.username_password_form',)

Using Multiple Backends

To use multiple authentication backends, set AUTHENTICATION_BACKEND to django.contrib.auth.backends. MultiAuthBackend in your settings file.You must also set AUTHENTICATION_BACKEND to a tuple of the backends you wish to use, in order.

For example:

AUTHENTICATION_BACKEND = 'django.contrib.auth.backends.MultiAuthBackend'

MULTIAUTH_BACKENDS = (
    'django.contrib.auth.backends.LDAPBackend',
    'django.contrib.auth.backends.ModelBackend',
)

When you call authenticate or get_user on MultiAuthBackend, it will in turn call the same method on each backend in MULTIAUTH_BACKENDS in order.

Note: In the multi-auth branch (2892), you need to set AUTHENTICATION_BACKENDS to a tuple, similar to MULTIAUTH_BACKENDS above. the authenticate method looks for this setting in your settings.py file. I have it working and all I have is:

AUTHENTICATION_BACKENDS = (
    "django.contrib.auth.copy_of_backends.LDAPBackend",
)

I made a copy of contrib.auth.backends so the svn can update it without overwriting my LDAPBackend class. Don't know if that is the best way to do it or not, but it works.

I also hacked the contrib.auth.models file to change the check_password function to check against our LDAP server, and added a few small functions to check the type of user account. I know this will break next time I update the source, but I have a copy of that as well. There is surely a better way, but I'm still learning.

Writing Backends

Authentication backends are pretty simple. They just need to implement 2 methods, authenticate and get_user.

backend.authenticate(self, credentials)

If the credentials match a user in this backend it returns a user object. If not, it returns None. Keep in mind that credentials could be a dict, a string, pretty much anything. You'll have to make sure that authenticate does the appropriate checking and returns None for credentials that it can't handle.

The user object will generally be an instance of django.contrib.auth.models.User, but really, it could be anything. You will need to at least fake the interface for django.contrib.auth.models.User if you want to use the admin system however. Your backend can create and save an instance of django.contrib.auth.models.User when a user logs in for the first time. You could also add them to a default set of groups at that time.

backend.get_user(self, user_id)

backend.get_user simply takes a user id and returns the user that matches that id. The user id is not neccessarily numeric, and in most cases it won't be. It could be a username, an email address, whatever. The important part is that it uniquely identifies a user.

sample LDAPBackend class

This is located in the contrib/auth/copy_of_backends.py file. The two original models are still in the file as well. I just added this one in the middle.

class LDAPBackend:
    """
    Authenticate against our LDAP Database
    """
    def authenticate(self, username=None, password=None):
        # bind and see if the user exists
        if ldap.userExists(username):
            # user exists in our LDAP, see if they exist in Django
            # if not, add them to django's user database since django relies on that
            try:
                user = User.objects.get(username=username)
                if ldap.check_ldap_password(username, password):
                    return user
            except User.DoesNotExist:
                # get the first name, last name, email from ldap
                u = ldap.getUser(username)
                # get user attributes here as well, like mail, fname, lname
                user = User(username=username, password='getmefromldap')
                user.email = mail
                user.first_name = fname
                user.last_name = lname
                user.is_staff = False
                user.is_superuser = False
                user.save()
                return user
        else:
            return None

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

And it worked! I was able to logon as a user who had no entry in Django, and then it added my entry and away I went. Pretty nice stuff.