LDAP Authentication in Django with Backends

     

After having written a custom LDAP authentication process for django before 'multi-auth' hit the trunk, I decided I'd rewrite what I wrote in the new and purportedly easier to use backends system. Rumors that it was chiefly because a subversion update had caused my old code to break are greatly exaggerated. (*cough*) However, I'm quite glad I decided to take the plunge. It seems Django's new backends system has really made multi-faceted authentication dead simple.

(N.B. I don't fancy myself much of a blogger – or a writer at all for that matter, so bear with me as I attempt my first real foray into making technical data exciting.)

Its goal is to allow users to be authenticated via a different source (or sources) than the standard Users table. How it manages this goal is via the AUTHENTICATION_BACKENDS setting in settings.py. This is a tuple of python paths, pointed towards classes, that will do the authentication for Django.

The classes are dead simple, too. You only need to define two functions to get the full authentication management off the ground! For example, here would be a basic class that would authenticate the user 'nobody':

from django.contrib.auth.models import User

class SimpleBackend:
    def authenticate(self, username=None, password=None):
        # Naturally, you would never do this. Always use hashed
        # passwords. This is merely for example purposes.
        if username == "nobody" and password == "somepassword":
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                # The user 'nobody' does not exist in Django's records.
                return None
        return user

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

The two functions you need to define are 'authenticate' and 'get_user', where authenticate does the meat of the work – figuring out if your user and password are valid, and get_user merely returns the user object given a user_id.

When it came to implement this functionality with LDAP, I had luckily already written a good portion of the code for my old, non-backend process. Essentially it went something like this.

  1. Determine if the user exists in LDAP.
  2. If they do, check their password against LDAP. if successful, get or create their user (if created, make sure they are given no permissions) and return
  3. If they do not exist, check their username against the django database and authenticate them there.

One of the coolest things about Django's new backend system is that step 3 is already done for us just by using a simple setting. Remember how before I said we change AUTHENTICATION_BACKENDS? Well, with a setting like this:

AUTHENTICATION_BACKENDS = (
 'sputnik.backends.ldapBackend.LDAPBackend',
 'django.contrib.auth.backends.ModelBackend',
)

That code will first attempt authentication against our LDAP class via sputnik.backends.ldapBackend.LDAPBackend, and if that returns no user, it will then check Django's built-in authentication.

Let's take a look at what it actually looks like to implement LDAP authentication with this backends functionality.

Actually, before we dive into that, I should explain the general steps that we go through when doing LDAP authentication for all of our non-Django web applications. It probably says more about the quirks of our setup than anything generally applicable about LDAP authentication, but it will quickly become apparent to you that we go through some extra steps. We are a school, and every user in LDAP is a member of either the faculty, staff, or student group, and that group membership is a part of a person's DN. And so we need to first do a bind with a known user to find out the DN of the user attempting to authenticate. Then after getting the user's full DN, we attempt a second bind with the password the user provided. Anyhow, now back to the show.

So for Django, I first created a directory in my main project directory called 'backends' (I have no idea whether this adheres to any standards – it just seemed the most logical place for me), and created a file ldapBackend.py within it. In that file, I have this code:

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

# Constants
AUTH_LDAP_SERVER = 'Your LDAP Server'
AUTH_LDAP_BASE_USER = "cn=Your, o=BaseUser"
AUTH_LDAP_BASE_PASS = "Your Base Password"

class LDAPBackend:
    def authenticate(self, username=None, password=None):
        base = "o=YourOrganization"
        scope = ldap.SCOPE_SUBTREE
        filter = "(&(objectclass=person) (cn=%s))" % username
        ret = ['dn']

        # Authenticate the base user so we can search
        try:
            l = ldap.open(AUTH_LDAP_SERVER)
            l.protocol_version = ldap.VERSION3
            l.simple_bind_s(AUTH_LDAP_BASE_USER,AUTH_LDAP_BASE_PASS)
        except ldap.LDAPError:
            return None

        try:
            result_id = l.search(base, scope, filter, ret)
            result_type, result_data = l.result(result_id, 0)

            # If the user does not exist in LDAP, Fail.
            if (len(result_data) != 1):
                return None

            # Attempt to bind to the user's DN
            l.simple_bind_s(result_data[0][0],password)

            # The user existed and authenticated. Get the user
            # record or create one with no privileges.
            try:
                user = User.objects.get(username__exact=username)
            except:
                # Theoretical backdoor could be input right here. We don't
                # want that, so input an unused random password here.
                # The reason this is a backdoor is because we create a
                # User object for LDAP users so we can get permissions,
                # however we -don't- want them able to login without
                # going through LDAP with this user. So we effectively
                # disable their non-LDAP login ability by setting it to a
                # random password that is not given to them. In this way,
                # static users that don't go through ldap can still login
                # properly, and LDAP users still have a User object.
                from random import choice
                import string
                temp_pass = ""
                for i in range(8):
                    temp_pass = temp_pass + choice(string.letters)
                user = User.objects.create_user(username,
                         username + '@carthage.edu',temp_pass)
                user.is_staff = False
                user.save()
            # Success.
            return user
           
        except ldap.INVALID_CREDENTIALS:
            # Name or password were bad. Fail.
            return None

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

This does all the work for us. Note that if you are using LDAP, there is a good chance your code will be even simpler, because in many cases you will not have to bind a base user before attempting to bind your user's username and password.

After that is created, you're essentially done. The main login interface will now use your LDAP authentication process first, and fall back to Django's standard authentication second. All in all a very easy, very natural way of coding this multi-layered authentication schema. I couldn't be happier with it.

22 Responses to “LDAP Authentication in Django with Backends”

  1. Chris Says:

    Edit: fixed a bug where we were checking if simple_bind succeeded with an if statement – which is unnecessary. It never returns a value, merely excepts on failure.

  2. Bjorn Says:

    Thanks for the nice article! A simpler way to create the temporary password is:

    temp_pass = User.objects.make_random_password(8)

    – bjorn

  3. Chris Says:

    Thanks, Bjorn! I'm continually amazed by the bits of functionality I miss in Django.

  4. JustJohnny Says:

    fantastic write-up! The J-School here at UT Knoxville is getting ready to release their django based, student ran, news web site. It took all of 5 minutes to adapt your code/idea to our needs. Thanks for putting this out on the net!

  5. Chris Says:

    Glad I could help, JJ. Just 'pay it forward' and release some of your code you feel might be worthwhile sometime in the future. :)

  6. adam stokes Says:

    Just for giggles, here is connecting over SSL, rest of the code is the same.

    # Constants
    AUTH_LDAP_SERVER = 'ldaps://example.com'

    class LDAPBackend:
    def authenticate(self, username=None, password=None):
    base = "ou=users,dc=example,dc=com"
    scope = ldap.SCOPE_SUBTREE
    filter = "(&(objectclass=person) (uid=%s))" % username
    ret = ['dn']

    # Authenticate the base user so we can search
    try:
    ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,'/etc/openldap/cacerts/mycert.pem')
    l = ldap.initialize(AUTH_LDAP_SERVER,trace_level=1,trace_file=sys.stderr)
    l.protocol_version = ldap.VERSION3
    l.bind_s(",",ldap.AUTH_SIMPLE)

  7. andrew Says:

    thanks for your article, it helps me a lot!
    I have another problem about LDAP and Django, and I can't find place to ask.
    After the LDAP authentication is done, I wonder if we can use the Django administration to manage the attributes in LDAP? For example, when I select a user in the admin page, I can edit extra information than Django provided such as "cn", "uid" attributes in LDAP.

  8. Chris Dary Says:

    Glad to help.

    I've never seen anything like that – but something built that would do that would probably be very useful. (Perhaps even released open source. :P Pay it forward and such!)

    You'd probably not be able to do it in the Django admin – you'd need to create a separate app for it.

  9. ross Says:

    Thanks for this.

    One trap to be mindful of. Our internal ldap server allows unauthenticated binds, so it is crucial to check that the supplied password is > " as simple_bind_s('foo',") will happily NOT return an exception no matter what 'foo' password really is!

    A good argument for not allowing anonymous binds even if everything is inside a firewall!

  10. TerryH Blog 雲 >>> » Blog Archive » Django POP3Backend Says:

    [...] LDAP Authentication in Django with Backends http://www.carthage.edu/webdev/?p=12 [...]

  11. Learning Django by Example(4): First user authenticated by Refactor the Life Says:

    [...] Though Gelman is designed for personal use, the multi-user functionality is added just in case you want to share you collection with family members and friends later.The built-in authentication is quite neat, if you deploy Gelman into an enterprise environment, you may consider this. Now, just follow the documentation, and add the very first reader to Gelman by running python manage.py shell: >>> from django.contrib.auth.models import User >>> u = User.objects.create_user('python', ", 'spam') >>> u.save() [...]

  12. Paulo Matos Says:

    There's also a more elaborated version of this functionality on django tracker.
    Take a look at ticket #2507:
    http://code.djangoproject.com/ticket/2507

  13. Paulo Matos Says:

    [this comment is way off-topic! Chris, please remove it once you read it]

    The image generator within "Kind-a-Captcha" (http://www.carthage.edu/webdev/wp-content/plugins/SK2/sk2_second_chance.php),
    you're using is broken, the png file generated has one extra "newline" character as the first byte, which destroys the "magic number" make the file unreadable. Removing that newline will solve the issue.

  14. frank Says:

    I'm trying authentication with ldap in django but I have a problem, the application said me that the module ldap doens't exist, what is the problem ????, I should download it ???

    please help me

    Exception Value: No module named ldap

  15. Rufman Says:

    Have you installed python-ldap? (http://python-ldap.sourceforge.net/). If yes are you using apache and mod_python?

  16. Nick Says:

    Thank you for this great tutorial !

    Regards,
    Open Source in Israel

  17. ldap authentication Says:

    [...] process for django before &39multi-auth&39 hit the trunk, I decided I&39d rewrite what I wrote …http://www.carthage.edu/webdev/?p=12LDAP authentication – MoodleDocs… to set up Lightweight Directory Access Protocol LDAP [...]

  18. Django gotchas - part 2 | Ulises' random assortments Says:

    [...] to writing your custom authentication module. There are plenty articles out there (like this and this) dealing with this topic and even the wonderful Django documentation explains how to do it very [...]

  19. va refinance Says:

    That is what i was doing wrong. I didn't realize the order. LDAP authentication process first, and fall back to Django's standard authentication second

  20. essentuki Says:

    Wonderfull…

  21. David Berger Says:

    Even better: user.set_unusable_password()

  22. GooglePeople Says:

    Using the statement user.set_unusable_password() parameters will fix this annoying problem with the least effort.

Leave a Reply