I’ve been trying to get some Django stuff running that can securely authenticate users against Windows Active Directory and also populate some info (first/last name, email address, maybe groups etc.). There are lots of resources out there but nothing was fully complete or modern and it took me some figuring/hacking to get it done.
Resources I found include:
- django-auth-ldap — the normal LDAP plugin. Problem: It does not natively support SASL and simple binds would send clear-text passwords. I think normal people would just activate TLS in this case but I didn’t want to do that
- A relevant SO post — with an answer linking to a useful snippet that no longer works on recent django versions.
- django-auth-ldap-ad — Someone’s entire different ldap plugin made specifically for this purpose. But it isn’t being maintained, is GPL-2, and doesn’t work directly in Python 3 or recent django.
I worked hard on that last one and actually got it running in Python 3, Django 2.1. But it still needed work (Needed a different signature in the authenticate
method, bytes vs. strings issues, etc.).
In the end I made a custom subclass of the LDAPBackend
in django-auth-ldap.
Activate the custom subclass and specify some settings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
from django_auth_ldap.config import LDAPSearch import ldap AUTHENTICATION_BACKENDS = [ 'nick.auth.LDAPSaslBackend', #'django_auth_ldap_ad.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ] AUTH_LDAP_SERVER_URI = "ldap://server.domain.tld" AUTH_LDAP_SEARCH_DN = "dc=domain,dc=tld" AUTH_LDAP_CONNECTION_OPTIONS = { ldap.OPT_REFERRALS: 0 # important to prevent constant disconnects } AUTH_LDAP_BIND_DN = "" AUTH_LDAP_BIND_PASSWORD = "" AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=domain,dc=tld", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)") AUTH_LDAP_USER_ATTR_MAP = {"first_name": "givenName", "last_name": "sn", "email":"mail"} |
Make the custom authenticator subclass in auth.py. Note that this is heavily based on the original class in the library. I did a few things with this that weren’t awesome but worked:
- Copy/pasted lots of code with _LDAPUser switched to my new subclass of that user. A better solution would be to change the library code to use a factory method and just update that. This subclass would shrink dramatically with that change.
- Overrode the attempt to know the DN before authenticating. SASL works fine with just the username.
- Eliminated re-binding when getting the connection. It was grabbing that through a property and failing authentication all the time because the auth_tokens weren’t cached anywhere. Another option would have been to cache the auth tokens.
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 |
import logging from django_auth_ldap.backend import LDAPBackend, _LDAPUser import ldap import ldap.sasl logger = logging.getLogger('nick') class LDAPSaslBackend(LDAPBackend): """Specialized SASL backend for Kerberos LDAP. See https://django-auth-ldap.readthedocs.io/en/latest/install.html """ def authenticate(self, request, username=None, password=None, **kwargs): logger.info('Trying to authenticate %s. ' % (username)) if password or self.settings.PERMIT_EMPTY_PASSWORD: ldap_user = SASLUser(self, username=username.strip(), request=request) user = self.authenticate_ldap_user(ldap_user, password) else: logger.debug("Rejecting empty password for {}".format(username)) user = None return user def get_user(self, user_id): user = None logger.info('Getting %s' % user_id) try: user = self.get_user_model().objects.get(pk=user_id) SASLUser(self, user=user) # This sets user.ldap_user except ObjectDoesNotExist: pass return user def get_group_permissions(self, user, obj=None): if not hasattr(user, "ldap_user") and self.settings.AUTHORIZE_ALL_USERS: SASLUser(self, user=user) # This sets user.ldap_user if hasattr(user, "ldap_user"): permissions = user.ldap_user.get_group_permissions() else: permissions = set() return permissions def populate_user(self, username): logger.info('populating %s' % username) ldap_user = SASLUser(self, username=username) user = ldap_user.populate_user() return user def authenticate_ldap_user(self, ldap_user, password): """ Returns an authenticated Django user or None. """ return ldap_user.authenticate(password) class SASLUser(_LDAPUser): def _authenticate_user_dn(self, password): """ Binds to the LDAP server with the user's DN and password. Raises AuthenticationFailed on failure. """ try: sticky = self.settings.BIND_AS_AUTHENTICATING_USER self._bind_as(None, password, sticky=sticky) except ldap.INVALID_CREDENTIALS: raise self.AuthenticationFailed("user DN/password rejected by LDAP server.") def _bind_as(self, bind_dn, bind_password, sticky=False): conn = self._get_connection() bind_dn = self._username auth_tokens = ldap.sasl.digest_md5(bind_dn, bind_password) conn.sasl_interactive_bind_s(bind_dn, auth_tokens) self._connection_bound = sticky @property def connection(self): return self._get_connection() |
And boom, it works! Users get authenticated and name/email gets populated. Quite glorious. I haven’t messed with groups much but that’s the next step.
Moving forward, I’ll consider cleaning this up and contributing it to django-auth-ldap.
Looking forward to giving this a crack over the next couple of days for something I’m working on. Is this still pretty reliable?
Thanks
Hi, Please help me about, How to integrate Windows Active Directory authentication with DJANGO 2.2
Please provide me the steps. Thanks for your previous valuable information.