Login

Authenticate against Active Directory - LDAP (my version)

Author:
trebor74hr
Posted:
March 26, 2009
Language:
Python
Version:
1.0
Score:
1 (after 1 ratings)

This is based on snippet 501, with some corrections:

  1. if user doesn't exist and AD.authenticate passes, then create new user - don't store password - prevent default django auth backend authentication
  2. if user exists and AD.authenticate passes - django User object is updated
  3. various error handling
  4. fixes (some mentioned in original snippet)
  5. some settings removed from settings to backend module
  6. other changes (ADUser class, re-indent, logging etc.)
  7. ignores problem with search_ext_s (DSID-0C090627)
  8. caching connection - when invalid then re-connect and try again

Note that there is also ldaps:// (SSL version) django auth backend on snippet 901.

Possible improvements:

  1. define new settings param - use secured - then LDAPS (snippet 901)
  2. define new settings extra ldap options - e.g. protocol version
  3. fetch more data from AD - fill in user data - maybe to make this configurable to be able to update user.get_profile() data too (some settings that has mapping AD-data -> User/UserProfile data)
  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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# ---------------------------------------- 
#          ActiveDirectoryBackend
# ---------------------------------------- 
#
# Created new snippet: 
#       http://www.djangosnippets.org/snippets/edit/1397/
# based on:
#       http://www.djangosnippets.org/snippets/501/
# similar snippet (ldaps:// - secured) is on: 
#       http://www.djangosnippets.org/snippets/901/
# 
# ---------- settings.py -------------------
# 
#     # Active directory auth backend setup 
#     # on win32 check env.vars:
#     #   USERDNSDOMAIN=EXAMPLE.COM
#     #   USERDOMAIN=EXAMPLE
#     AD_DNS_NAME = 'domaindnsname.local'
#     AD_NT4_DOMAIN = 'DOMAINNAME' # This is the NT4/Samba domain name
#     AD_SEARCH_DN = 'dc=domaindnsname,dc=local'
#     AD_LDAP_PORT = 389
# 
#     AUTHENTICATION_BACKENDS = (
#                               'company.django.auth.ActiveDirectoryBackend',
#                               # if you want to have default django backend too
#                               'django.contrib.auth.backends.ModelBackend',
#                               )
#     
import ldap
import logging
from django.contrib.auth.models import User
from django.contrib.auth.backends import ModelBackend
from django.conf import settings

logger = logging.getLogger()

# -------------------------------------------

# general function - belongs to utils
def get_exc_str(bClear=False):
   import sys, traceback
   x=sys.exc_info()
   if not x[0]:
      return "No py exception"
   out="%s/%s/%s" % (str(x[0]), str(traceback.extract_tb(x[2])), str(x[1]))
   if bClear:
      sys.exc_clear()
   return out

# --------------------------------------------

class ADUser(object):

    # class level, makes "operational error" problem by search occurs less
    ldap_connection = None 
    AD_SEARCH_FIELDS = ['mail','givenName','sn','sAMAccountName']

    @classmethod
    def get_ldap_url(cls):
        return 'ldap://%s:%s' % (settings.AD_DNS_NAME, settings.AD_LDAP_PORT)

    def __init__(self, username):
        self.username = username
        self.user_bind_name = "%s@%s" % (self.username, settings.AD_NT4_DOMAIN)
        self.is_bound = False
        self.has_data = False

        self.first_name = None
        self.last_name = None
        self.email = None

    def connect(self, password):
        had_connection = ADUser.ldap_connection is not None
        ret = self._connect(password)
        # WORKAROUND: for invalid connection
        if not ret and had_connection and ADUser.ldap_connection is None: 
            logger.warning("AD reset connection - invalid connection, try again with new connection")
            ret = self._connect(password)
        return ret

    def _connect(self, password):
        if not password:
            return False # Disallowing null or blank string as password
        try:
            if ADUser.ldap_connection is None:
                logger.info("AD auth backend ldap connecting")
                ADUser.ldap_connection = ldap.initialize(self.get_ldap_url())
                assert self.ldap_connection==ADUser.ldap_connection # python won't do that ;)
            self.ldap_connection.simple_bind_s(self.user_bind_name,password)
            self.is_bound = True
        except Exception, e:
            if str(e.message).find("connection invalid")>=0:
                logger.warning("AD reset connection - it looks like invalid: %s (%s)" % (str(e),  get_exc_str()))
                ADUser.ldap_connection = None
            else:
                logger.error("AD auth backend ldap - probably bad credentials: %s (%s)" % (str(e),  get_exc_str()))
            return False
        return True

    def disconnect(self):
        if self.is_bound:
            logger.info("AD auth backend ldap unbind")
            self.ldap_connection.unbind_s()
            self.is_bound=False
            
    def get_data(self):
        try:
            assert self.ldap_connection
            # NOTE: Something goes wrong in my case - ignoring this until solved :(
            #       {'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'}
            res = self.ldap_connection.search_ext_s(settings.AD_SEARCH_DN,
                                 ldap.SCOPE_SUBTREE, 
                                 "sAMAccountName=%s" % self.username,
                                 self.AD_SEARCH_FIELDS)
            self.disconnect()
            if not res:
                logger.error("AD auth ldap backend error by searching %s. No result." % settings.AD_SEARCH_DN)
                return False
            assert len(res)>=1, "Result should contain at least one element: %s" % res
            result = res[0][1]
        except Exception, e:
            self.disconnect()
            logger.error("AD auth backend error by fetching ldap data: %s (%s)" % (str(e),  get_exc_str()))
            return False

        try:
            self.first_name = None
            if result.has_key('givenName'):
                self.first_name = result['givenName'][0]

            self.last_name = None
            if result.has_key('sn'):
                self.last_name = result['sn'][0]

            self.email = None
            if result.has_key('mail'):
                self.email = result['mail'][0]
            self.has_data = True        
        except Exception, e:
            logger.error("AD auth backend error by reading fetched data: %s (%s)" % (str(e),  get_exc_str()))
            return False

        return True

    def __del__(self):
        try:
            self.disconnect()
        except Exception, e:
            logger.error("AD auth backend error when disconnecting: %s (%s)" % (str(e),  get_exc_str()))
            return False

    def __str__(self):
        return "AdUser(<%s>, connected=%s, is_bound=%s, has_data=%s)" % (self.username, self.ldap_connection is not None, self.is_bound, self.has_data)

# -------------------------------------------

class ActiveDirectoryBackend(ModelBackend):

    def authenticate(self,username=None,password=None):
        logger.info("AD auth backend for %s" % username)
        aduser = ADUser(username)

        if not aduser.connect(password):
            return None

        user = None
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            user = User(username=username, is_staff = False, is_superuser = False)

        if not aduser.get_data():
            logger.warning("AD auth backend failed when reading data for %s. User detail data won't be updated in User model." % username)
        else:
            # NOTE: update user data exchange to User model
            assert user.username==aduser.username
            user.first_name=aduser.first_name
            user.last_name=aduser.last_name
            user.email=aduser.email
            #user.set_password(password)
            logger.warning("AD auth backend overwriting auth.User data with data from ldap for %s." % username)
        user.save()
        logger.info("AD auth backend check passed for %s" % username)
        return user

    # NOTE: no need to implement get_user - ModelBackend.get_user is all I need

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 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

mkorman (on April 7, 2009):

I believe ActiveDirectoryBackend should inherit from django.contrib.auth.backends.ModelBackend, or else permissions will not work.

#

trebor74hr (on April 10, 2009):

Thanks, now is based on ModelBackend. Other things I did is:

  • refactoring - reorganized
  • ignores problem with search_ext_s - for me it happens at first connection boot (see in code "DSID-0C090627")
  • ldap_connection is on class level - makes the problem DSID-0C090627 occurs less
  • more error handling, more log output

#

TechnicalBard (on July 7, 2009):

where does it store the log file?

#

pebe (on February 26, 2010):

try it with

ldap.set_option(ldap.OPT_REFERRALS, 0)

right after importing the ldap module.

This will let you override the backend auth.User data with data from ldap.

#

Please login first before commenting.