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':
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.
- Determine if the user exists in LDAP.
- 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
- 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:
'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:
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.





August 22nd, 2006 at 2:20 pm
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.
September 13th, 2006 at 3:13 am
Thanks for the nice article! A simpler way to create the temporary password is:
temp_pass = User.objects.make_random_password(8)
– bjorn
September 13th, 2006 at 10:15 am
Thanks, Bjorn! I'm continually amazed by the bits of functionality I miss in Django.
October 9th, 2006 at 5:35 pm
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!
October 10th, 2006 at 2:16 pm
Glad I could help, JJ. Just 'pay it forward' and release some of your code you feel might be worthwhile sometime in the future.
December 29th, 2006 at 4:29 pm
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)
March 13th, 2007 at 2:38 am
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.
March 13th, 2007 at 10:15 am
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.
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.
March 17th, 2007 at 11:30 am
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!
April 27th, 2007 at 2:12 am
[...] LDAP Authentication in Django with Backends http://www.carthage.edu/webdev/?p=12 [...]
October 3rd, 2007 at 10:52 am
[...] 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() [...]
December 10th, 2007 at 7:23 pm
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
December 10th, 2007 at 7:34 pm
[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.
February 11th, 2008 at 6:11 pm
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
February 20th, 2008 at 10:34 am
Have you installed python-ldap? (http://python-ldap.sourceforge.net/). If yes are you using apache and mod_python?
March 27th, 2008 at 11:21 am
Thank you for this great tutorial !
Regards,
Open Source in Israel
May 6th, 2008 at 3:37 pm
[...] 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 [...]
October 14th, 2008 at 5:53 am
[...] 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 [...]
March 1st, 2009 at 2:22 am
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
April 30th, 2009 at 9:52 pm
Wonderfull…
May 19th, 2009 at 4:23 pm
Even better: user.set_unusable_password()
May 31st, 2009 at 1:05 pm
Using the statement user.set_unusable_password() parameters will fix this annoying problem with the least effort.