Login

Authenticate against Active Directory

Author:
jfray2
Posted:
December 9, 2007
Language:
Python
Version:
.96
Score:
14 (after 14 ratings)

I put this in a file called auth.py, then referenced it in the settings.py like so:

AUTHENTICATION_BACKENDS = ('myproject.myapp.auth.ActiveDirectoryBackend',)

This has been tested on my office network, with the following setup:

Django 0.96
Python 2.4.4
python-ldap
Fedora Core 5 (On the server hosting Django)
AD Native Mode
2 Windows 2003 AD servers
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
-- Added to settings.py --

### ACTIVE DIRECTORY SETTINGS

# AD_DNS_NAME should set to the AD DNS name of the domain (ie; example.com)  
# If you are not using the AD server as your DNS, it can also be set to 
# FQDN or IP of the AD server.

AD_DNS_NAME = 'example.com'
AD_LDAP_PORT = 389

AD_SEARCH_DN = 'CN=Users,dc=example,dc=com'

# This is the NT4/Samba domain name
AD_NT4_DOMAIN = 'EXAMPLE'

AD_SEARCH_FIELDS = ['mail','givenName','sn','sAMAccountName']

AD_LDAP_URL = 'ldap://%s:%s' % (AD_DNS_NAME,AD_LDAP_PORT)


-- In the auth.py file --

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

class ActiveDirectoryBackend:

  def authenticate(self,username=None,password=None):
    if not self.is_valid(username,password):
      return None
    try:
      user = User.objects.get(username=username)
    except User.DoesNotExist:
      l = ldap.initialize(settings.AD_LDAP_URL)
      l.simple_bind_s(username,password)
      result = l.search_ext_s(settings.AD_SEARCH_DN,ldap.SCOPE_SUBTREE, 
               "sAMAccountName=%s" % username,settings.AD_SEARCH_FIELDS)[0][1]
      l.unbind_s()

      # givenName == First Name
      if result.has_key('givenName'):
        first_name = result['givenName'][0]
      else:
        first_name = None

      # sn == Last Name (Surname)
      if result.has_key('sn'):
        last_name = result['sn'][0]
      else:
        last_name = None

      # mail == Email Address
      if result.has_key('mail'):
        email = result['mail'][0]
      else:
        email = None

      user = User(username=username,first_name=first_name,last_name=last_name,email=email)
      user.is_staff = False
      user.is_superuser = False
      user.set_password(password)
      user.save()
    return user

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

  def is_valid (self,username=None,password=None):
    ## Disallowing null or blank string as password
    ## as per comment: http://www.djangosnippets.org/snippets/501/#c868
    if password == None or password == '':
      return False
    binddn = "%s@%s" % (username,settings.AD_NT4_DOMAIN)
    try:
      l = ldap.initialize(settings.AD_LDAP_URL)
      l.simple_bind_s(binddn,password)
      l.unbind_s()
      return True
    except ldap.LDAPError:
      return False

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 1 week ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 2 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

arne (on December 10, 2007):

I had this on my to-do list for quite some time, thanks for sharing.

#

arnar (on December 11, 2007):

Very nice, thanks. Perhaps it should fetch the configuration variables from the settings module though.

#

jfray2 (on December 11, 2007):

Yeah, I was thinking about that. I use ConfigParser and an external .ini file in my installation. For the snippet, I just wanted to have everything in the same place for readability. I'll update once I figure out how to move the variables to the settings module.

#

jfray2 (on December 11, 2007):

I've gone ahead and moved it to the settings.py and updated the snippet to reflect this. I also updated the variable names so that they're a little more settings.py-ish and removed the unnecessary call to urlunparse.

#

brooks_lt (on December 17, 2007):

Forgive my ignorance, but I just want to clarify that this authenticates against the AD server each time a user logs in, not just the first?

#

jfray2 (on December 18, 2007):

Yes, since the is_valid method is called on every authentication, it will attempt to bind each time a user logs in.

One thing I was thinking of was changing the user.is_staff and the user.is_superuser to grab that info based on group membership in AD. Maybe a mapping of Domain Users to is_staff and Administrators to is_superuser?

#

ipse (on January 2, 2008):

I think it would be good to tag this snippet as 'auth' or so, as other authentification snippets do.

#

raman (on April 24, 2008):

BE CAREFUL with line 83. Don't allow a blank or null password.

Here's possible scenario:

(1) enter a valid username and blank password

(2) "is_valid" (line 78) returns a True (according to LDAP protocol)

(3) the user's fname, lname, email is found via the search (line 44)

(4) the user (with a blank pw) becomes a superuser

(Thanks for the snippet!)

#

jfray2 (on May 2, 2008):

Thanks for the heads up -- I'm fixing the snippet here. If you have a good suggestion on how to better manage staff/superusers , I'd be glad to integrate it here. I'm testing locally an idea I had about group membership..basically Domain Users will be is_staff, and Administrators will be is_superuser. Once I have it working correctly, I'll post it here.

#

max3 (on May 23, 2008):

Foremost thanks for you snippet.

  • Why not to use user's login and password for binding in authenticate() instead of creating GuestBindAccount?

  • ldap.open is deprecated, so ldap.initialize would be better to use, IMHO.

  • To my mind

user.is_staff = False and user.is_superuser = False

as defaults are safer. Mapping to domain groups is not obviously suits to every project.

#

jfray2 (on June 4, 2008):

@max3: Thank you for your thanks! To answer your points:

1) In the authenticate method, the self.is_valid call uses the user's login and password to authenticate. If it fails due to the user not existing (User.DoesNotExist), the method still needs to be able to bind to be complete the task, thus the GuestBindAccount credentials.

2) Thanks for the heads up. I'm reading up on the usage for ldap.initialize and will update this snippet shortly.

3) I've changed the snippet to make user.is_staff and user.is_superuser False by default. You're right, not every project will have clean group mappings for these attributes.

#

jfray2 (on June 4, 2008):

continued from above post..

Duh :) I'm way off. Of course the user will exist. It's been a few months since I've really looked at this snippet deeply. I'll go ahead and make the changes to the snippet to remove the need for the GuestBindAccount. Thanks max3!

#

jfray2 (on June 4, 2008):

I'm having a dead brain day. Didn't realize that I was already using ldap.initialize earlier. I went ahead and removed the requirement for the GuestBindAccount and also realized that I didn't unbind ldap in the authenticate method.

Sorry for the comment spam!

#

ascott (on June 6, 2008):

Could you please explain why the is_valid method uses the AD_NT4_DOMAIN when binding whereas authenticate uses only the username? I'm getting an invalid credentials error after passing the is_valid test when binding for the second time to find the user details.

{'info': '80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 525, vece', 'desc': 'Invalid credentials'}

If I change either of the binds to match the other I receive the following error:

{'info': '00000000: LdapErr: DSID-0C090627, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, vece', 'desc': 'Operations error'}

I've tried both styles of username; the ones we log into AD with "Forename.Surname" and "Surname, Forename". Each one works for the appropriate bind call but then fails on the search.

Any ideas? Thanks.

#

Tarken (on September 22, 2008):

I had the same initial problem as ascott, but I was able to remedy it with the fix he initially suggested.

At line 14 of auth.py, instead of:

l.simple_bind_s(username,password)

I now have:

binddn = "%s@%s" % (username, settings.AD_NT4_DOMAIN)
l.simple_bind_s(binddn, password)

Works like a charm for me otherwise! Thanks for the snippet!

#

zhaoz (on December 25, 2008):

It seems like this snippet currently creates a user even if it already exists.

Shouldn't it return user on a succesful User.objects.get?

#

zhaoz (on December 25, 2008):

In fact, it seems as if the user does exist, then result is never set and auth throws an exception.

#

ohshaughnessy (on October 19, 2009):

This is just an outstanding snippet. I've found that if the user doesn't exist in Django, though, there can be a really long wait and an eventual timeout before the search function returns. I don't think this is necessarily common, but it's happening on searches against my AD server. I found that the delay can be avoided by preventing the search from following referrals. Right after "l = ldap.initialize()", set this option:

l.set_option(ldap.OPT_REFERRALS, 0)

If you're having trouble with long delays and timeouts, try this. It cured the problem for me.

#

mengjian (on April 8, 2010):

谢谢了,很好用

#

blurback (on July 15, 2010):

When using this code with the django admin, I had an issue with permissions. Unless superuser, the admin would always say "You don't have permission to edit anything" despite what was in the database. (using django 1.2)

The solution was to extend the default model backend:

    from django.contrib.auth.backends import ModelBackend  
    class ActiveDirectoryBackend(ModelBackend):

and then get_all_permissions, has_perm, has_module_perms worked like they should. Also, defining "get_user" was no longer necessary.

#

DianeOfTheMoon (on September 8, 2011):

If anyone runs into the same issue as ascott or Tarken, you should be able to change the section on creating a user from:

l = ldap.initialize(settings.AD_LDAP_URL)
l.simple_bind_s(username,password)

to

l = ldap.initialize(settings.AD_LDAP_URL)
l.set_option(ldap.OPT_REFERRALS, 0)
binddn = "%s@%s" % (username, settings.AD_NT4_DOMAIN)
l.simple_bind_s(binddn, password)

To get it to work correctly.

#

mfisher (on December 8, 2011):

Thanks for this! It was a huge help for me. One thing that was causing me trouble, though, is that Windows allows the username to be case-insensitive. I had some trouble with my end-users taking advantage of this (and getting multiple accounts). To address this, I wrote the following patch to force a consistent (lower) case in the Django Users database:

--- /usr/local/share/django-common/auth.py      2010-10-14 12:45:03.000000000 -0400
+++ auth.py     2011-11-08 18:21:44.000000000 -0500
@@ -12,7 +12,7 @@
             logging.debug("invalid username: %s" % username)
             return None
         try:
-            user = User.objects.get(username=username)
+            user = User.objects.get(username=username.lower())
         except User.DoesNotExist:
             l = ldap.initialize(settings.AD_LDAP_URL)
             l.set_option(ldap.OPT_REFERRALS, 0)
@@ -45,7 +45,7 @@
             else:
                 email = None

-            user = User(username=username,
+            user = User(username=username.lower(),
                         first_name=first_name,
                         last_name=last_name,
                         email=email)
@@ -67,7 +67,7 @@
         if password == None or password == '':
             logging.debug("null password received")
             return False
-        binddn = "%s@%s" % (username, settings.AD_NT4_DOMAIN)
+        binddn = "%s@%s" % (username.lower(), settings.AD_NT4_DOMAIN)
         logging.debug("binddn = %s" % binddn)
         try:
             l = ldap.initialize(settings.AD_LDAP_URL)

#

dack (on March 5, 2012):

After ldap.initialize, you should do:

l.set_option(ldap.OPT_REFERRALS, 0)

otherwise you may get errors like: {'info': '00000000: LdapErr: DSID-0C090627, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, vece', 'desc': 'Operations error'}

#

Please login first before commenting.