Feb
01
2006

Hacking in LDAP support to Django

I finally started using Django in a production app at work. I need to add username/password authentication, but I don’t want YANP (Yet Another Name and Password) for my users to remember. The obvious solution is to hook into ThoughtWorks’ internal LDAP server. But alas, Django does not have LDAP support built-in.

However, it was very easy to add this support myself. From start to finish it took me about an hour, most of which was spent just googling for sample code. And it’s taken me about 3 times as long to write this post, which probably explains why hackers prefer writing code over writing documentation.

Some assumptions: 1) You already have a working Django site you can log in and out of successfully using Django’s standard username and password scheme. 2) You have a working LDAP server on your network loaded with user accounts. 3) You know next to nothing about LDAP, other than that you want to use it to check names and passwords from your web app.

Step 1: Download and install the python-ldap library.

Go to http://python-ldap.sourceforge.net/ and follow the links to download the appropriate version of the library for your version of Python. It is out of scope here to tell you how to do this in detail. However, I hope this step can be replaced with a simple one-liner “easy_install ldap” at some point in the future.

FYI: I’m running Python 2.4.2 on Windows XP with Django (svn rev 2203)

Step 2: JFGI (Just Freakin’ Google It)

Most of the hour I spent on this task was googling around for example code authenticating against an LDAP server.

There are many examples available on the web, but I found this one most helpful. Feel free to read the whole post, but I only read as far as paragraph seven to find the code I needed.

I guess now that I “just freakin’ googled it” for you, you can skip this step and move on to step 3. However, I’ll just note that the above example was the best example I could find, but it was link #73 in the result set for the search terms “python ldap example”. I hope my post here raises the page’s rank a bit.

Step 3: Baby Steps — Interactive LDAP authentication.

My next task was to get LDAP authentication working with “plain vanilla” Python (No Django, just Python). It is good practice to use an interactive Python session when playing around with new code and new libraries, especially when your new code is likely to be wrong on the first try. This is a place where Python, Ruby, even SQL databases really shine. You can try out bad code and get feedback exceedingly quick. I guess this can be summed up with the mantra “Fail Faster”.

Now ask the system administrator of your LDAP server for two things: 1) The LDAP server’s address (e.g. “ldap.example.com”) 2) A sample “User DN” string (e.g. “uid=<user id>, ou=People,dc=Corporate,dc=Example,dc=COM”)

You’ll also need a name and password to test with, of course.

Now fire up an interactive session of Python and enter the following at the prompt:

>>> import ldap

l = ldap.open('ldap.example.com') username = 'jasonh' password = 'jasonh' userdn = 'uid=%s,ou=People,dc=Corporate,dc=Example,dc=COM' % username l.simplebinds(userdn,password)

If it all works correctly, nothing will happen. If there are any errors, an LDAP exception will be raised.

Let's clean up this code and catch the common errors:

>>> import ldap
>>> try:
...     username = 'jasonh'
...     password = 'jasonh'
...     user_dn = 'uid=%s,ou=People,dc=Corporate,dc=Example,dc=COM' % username
...     l = ldap.open('wrong_server.example.com')
...     l.simple_bind_s(user_dn,password)      
... except ldap.LDAPError, error:           
...     print 'problem with ldap',error
...
problem with ldap {'desc': 'Can't contact LDAP server'}
>>>

Step 4: Hacking Django

Now that we know we have Python and LDAP working correctly, let’s hack this support into Django.

Passwords are checked by the check_password() method in the User model (django\models\auth.py)

I didn’t want to completely gut the existing checkpassword method, especially since I want to keep using it for all ’superuser’ accounts in my app. So I simply renamed the old checkpassword method to “checkpassword” and typed in my new method.

Here’s the code:

    def checkpassword(self, rawpassword):
        '''
        Returns a boolean of whether the rawpassword was correct. Handles
        connection to ldap server for all non-superuser accounts.
        '''
        if self.issuperuser:
            # Use the normal Django auth system for superuser admin accounts.
            # Useful in case there are problems with the LDAP server.
            return self.checkpassword(rawpassword)
        else:
            # Use LDAP for everyone else...
            # One caveat, though. A user account must exist in Django for each user in LDAP.
            import ldap
            try:
                from django.conf.settings import AUTHLDAPDN, AUTHLDAPSERVER
distinguished
name = AUTHLDAPDN % self.username l = ldap.open(AUTHLDAPSERVER) l.simplebinds(distinguishedname,rawpassword) return True
except ldap.INVALID_CREDENTIALS, error: # Name or password were bad, or user doesn't exist. return False
except ldap.LDAPError, error:
# Catch all other LDAP errors raise error, 'Problem connecting to LDAP server.'

Two notes: 1) I added two things to my settings.py file:

AUTHLDAPSERVER = 'ldap.example.com' AUTHLDAPDN = 'uid=%s,ou=People,dc=Corporate,dc=Example,dc=COM'

2) Even though the password checking will be done against LDAP, all users of this app must also have an account in the regular Django database. Perhaps later, I’ll write some sync script to add/remove accounts from Django’s database based on the list of accounts in LDAP. But for now, since I only have a handful of production users, adding those accounts manually in Django is good enough for me.

Step 5: It’s time to decorate!

Now that I’ve wired up LDAP authentication to Django, I need to test it in the context of a real web session in the browser. Here I already had a working app, I simply need to add authentication security to my views. After inspecting Django’s “admin” application for an example, I noticed the use of the “staffmemberrequired” decorator to secure various views.

So I added two lines to my custom view (myproject\myapp\views.py):

At the top:

from django.contrib.admin.views.decorators import staffmemberrequired And as a decorator to each view:

@staffmemberrequired

I saved and reloaded the browser page. And everything just worked. Sweet. The caveat to this is that each user account must have “is_staff” set to True in the database.

After I got this working, I checked the official Django docs on how I should have officially secured my views. The documentation mentions adding the ‘loginrequired’ decorator, but it is not as simple as the doc says. According to the doc, with the help of comments posted at the bottom, to get the loginrequired decorator to work properly, you need to write a wrapper function to your view, modify your URLconf, and create a new template for the login form. I did those things and I still couldn’t get it to work, so I just went back to using the easier-to-use “staffmemberrequired” decorator. I believe the loginrequired decorator should be tweaked to work as simply as the the staffmember_required view.

Step 6 – There is no step 6!

This post could have been a simple patch posted to the Django issue tracker, but I wanted to show how easy it is to “hack” in your own authentication scheme if you need. It comes down to “overriding” the check_password method in the User model and doing whatever you want to it. And make sure you add any configuration bits to your settings.py file, so important stuff like server names are not hard coded in the authentication code.

posted in django, python by Jason Huggins

5 Comments to "Hacking in LDAP support to Django"

  1. S2使ったら負けかなと思っている wrote:

    [Django]今日のDjango

    認証にLDAPを使うの巻。 確かにパスワードを複数覚えるのはめんどい。 builtin、あるいは正式版なpluginでもいい機能だと思うけどどーかな。 Admin系ツールのカスタマイズ方法 TinyMCEは知って…

  2. Ian Holsman wrote:

    I did a similar thing ages ago, but let the Apache Web server handle all the dirty bits of ldap.

    it was acomplished via a middleware bit which recognized HTTPREMOTEUSER and created the user object via that.

    http://svn.zilbo.com/svn/django/common/middleware/httpauth.py

  3. Ben wrote:

    This is nice. I’m just learning Django, but good LDAP support is critical for many apps. Thanks!

  4. Leonardo Santagada wrote:

    Maybe you can get this on django, as a lot of people would like to use it. Maybe a more general tool where you simplify the work for any auth lib, the ones that comes to mind are LDAP, pam and some for NT domain or NIS+.

  5. Matthew Nalley wrote:

    Is it absolutely not possible to use LDAP exclusively? I don’t see why it’s necessary to duplicate every user in the Django users db table. Using LDAP in Django should be an alternative, not an addition.

 
Powered by Wordpress and MySQL. Theme by openark.org