{"id":1674,"date":"2018-08-18T17:54:20","date_gmt":"2018-08-19T00:54:20","guid":{"rendered":"https:\/\/partofthething.com\/thoughts\/?p=1674"},"modified":"2018-08-18T17:55:23","modified_gmt":"2018-08-19T00:55:23","slug":"authenticating-and-populating-users-in-django-using-a-windows-active-directory-and-sasl","status":"publish","type":"post","link":"https:\/\/partofthething.com\/thoughts\/authenticating-and-populating-users-in-django-using-a-windows-active-directory-and-sasl\/","title":{"rendered":"Authenticating and populating users in Django using a Windows Active Directory and SASL"},"content":{"rendered":"<p>I&#8217;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.<\/p>\n<p>Resources I found include:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/django-auth-ldap\/django-auth-ldap\">django-auth-ldap<\/a> &#8212; 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&#8217;t want to do that<\/li>\n<li><a href=\"https:\/\/stackoverflow.com\/questions\/410392\/using-ad-as-authentication-for-django\">A relevant SO post<\/a> &#8212; with an answer linking to <a href=\"https:\/\/djangosnippets.org\/snippets\/901\/\">a useful snippet<\/a> that no longer works on recent django versions.<\/li>\n<li><a href=\"https:\/\/github.com\/susundberg\/django-auth-ldap-ad\">django-auth-ldap-ad<\/a> &#8212; Someone&#8217;s entire different ldap plugin made specifically for this purpose. But it isn&#8217;t being maintained, is GPL-2, and doesn&#8217;t work directly in Python 3 or recent django.<\/li>\n<\/ul>\n<p><!--more--><\/p>\n<p>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.).<\/p>\n<p>In the end I made a custom subclass of the `LDAPBackend` in django-auth-ldap.<\/p>\n<p>Activate the custom subclass and specify some settings:<\/p>\n<pre class=\"lang:1c-zapros decode:true\">from django_auth_ldap.config import LDAPSearch\r\nimport ldap\r\n\r\nAUTHENTICATION_BACKENDS = [\r\n    'nick.auth.LDAPSaslBackend',\r\n    #'django_auth_ldap_ad.backend.LDAPBackend',\r\n    'django.contrib.auth.backends.ModelBackend',\r\n]\r\n\r\nAUTH_LDAP_SERVER_URI = \"ldap:\/\/server.domain.tld\"\r\nAUTH_LDAP_SEARCH_DN = \"dc=domain,dc=tld\"\r\n\r\nAUTH_LDAP_CONNECTION_OPTIONS = {\r\n    ldap.OPT_REFERRALS: 0  # important to prevent constant disconnects\r\n}\r\n\r\nAUTH_LDAP_BIND_DN = \"\"\r\nAUTH_LDAP_BIND_PASSWORD = \"\"\r\nAUTH_LDAP_USER_SEARCH = LDAPSearch(\"dc=domain,dc=tld\",\r\n    ldap.SCOPE_SUBTREE, \"(sAMAccountName=%(user)s)\")\r\nAUTH_LDAP_USER_ATTR_MAP = {\"first_name\": \"givenName\", \"last_name\": \"sn\", \"email\":\"mail\"}\r\n<\/pre>\n<p>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&#8217;t awesome but worked:<\/p>\n<ul>\n<li>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.<\/li>\n<li>Overrode the attempt to know the DN before authenticating. SASL works fine with just the username.<\/li>\n<li>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&#8217;t cached anywhere. Another option would have been to cache the auth tokens.<\/li>\n<\/ul>\n<pre class=\"lang:python decode:true\">import logging\r\n\r\nfrom django_auth_ldap.backend import LDAPBackend, _LDAPUser\r\nimport ldap\r\nimport ldap.sasl\r\n\r\nlogger = logging.getLogger('nick')\r\n\r\nclass LDAPSaslBackend(LDAPBackend):\r\n        \"\"\"Specialized SASL backend for Kerberos LDAP.\r\n\r\n        See https:\/\/django-auth-ldap.readthedocs.io\/en\/latest\/install.html\r\n\r\n        \"\"\"\r\n        def authenticate(self, request, username=None, password=None, **kwargs):\r\n                logger.info('Trying to authenticate %s. ' % (username))\r\n                if password or self.settings.PERMIT_EMPTY_PASSWORD:\r\n                    ldap_user = SASLUser(self, username=username.strip(), request=request)\r\n                    user = self.authenticate_ldap_user(ldap_user, password)\r\n                else:\r\n                    logger.debug(\"Rejecting empty password for {}\".format(username))\r\n                    user = None\r\n\r\n                return user\r\n\r\n        def get_user(self, user_id):\r\n                user = None\r\n\r\n                logger.info('Getting %s' % user_id)\r\n                try:\r\n                    user = self.get_user_model().objects.get(pk=user_id)\r\n                    SASLUser(self, user=user)  # This sets user.ldap_user\r\n                except ObjectDoesNotExist:\r\n                    pass\r\n\r\n                return user\r\n\r\n        def get_group_permissions(self, user, obj=None):\r\n                if not hasattr(user, \"ldap_user\") and self.settings.AUTHORIZE_ALL_USERS:\r\n                    SASLUser(self, user=user)  # This sets user.ldap_user\r\n\r\n                if hasattr(user, \"ldap_user\"):\r\n                    permissions = user.ldap_user.get_group_permissions()\r\n                else:\r\n                    permissions = set()\r\n\r\n                return permissions\r\n\r\n        def populate_user(self, username):\r\n                logger.info('populating  %s' % username)\r\n                ldap_user = SASLUser(self, username=username)\r\n                user = ldap_user.populate_user()\r\n\r\n                return user\r\n\r\n        def authenticate_ldap_user(self, ldap_user, password):\r\n                \"\"\"\r\n                Returns an authenticated Django user or None.\r\n                \"\"\"\r\n                return ldap_user.authenticate(password)\r\n\r\n\r\nclass SASLUser(_LDAPUser):\r\n        def _authenticate_user_dn(self, password):\r\n            \"\"\"\r\n            Binds to the LDAP server with the user's DN and password. Raises\r\n            AuthenticationFailed on failure.\r\n            \"\"\"\r\n            try:\r\n                sticky = self.settings.BIND_AS_AUTHENTICATING_USER\r\n                self._bind_as(None, password, sticky=sticky)\r\n            except ldap.INVALID_CREDENTIALS:\r\n                raise self.AuthenticationFailed(\"user DN\/password rejected by LDAP server.\")\r\n\r\n\r\n        def _bind_as(self, bind_dn, bind_password, sticky=False):\r\n                conn = self._get_connection()\r\n                bind_dn = self._username \r\n                auth_tokens = ldap.sasl.digest_md5(bind_dn, bind_password)      \r\n                conn.sasl_interactive_bind_s(bind_dn, auth_tokens)\r\n                self._connection_bound = sticky\r\n     \r\n        @property\r\n        def connection(self):\r\n            return self._get_connection()\r\n   \r\n\r\n      \r\n<\/pre>\n<p>And boom, it works! Users get authenticated and name\/email gets populated. Quite glorious. I haven&#8217;t messed with groups much but that&#8217;s the next step.<\/p>\n<p>Moving forward, I&#8217;ll consider cleaning this up and contributing it to django-auth-ldap.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;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 &hellip; <a href=\"https:\/\/partofthething.com\/thoughts\/authenticating-and-populating-users-in-django-using-a-windows-active-directory-and-sasl\/\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">Authenticating and populating users in Django using a Windows Active Directory and SASL<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"activitypub_content_warning":"","activitypub_content_visibility":"","activitypub_max_image_attachments":4,"activitypub_interaction_policy_quote":"anyone","activitypub_status":"","footnotes":""},"categories":[3],"tags":[],"class_list":["post-1674","post","type-post","status-publish","format-standard","hentry","category-computers"],"_links":{"self":[{"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/posts\/1674","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/comments?post=1674"}],"version-history":[{"count":2,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/posts\/1674\/revisions"}],"predecessor-version":[{"id":1676,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/posts\/1674\/revisions\/1676"}],"wp:attachment":[{"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/media?parent=1674"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/categories?post=1674"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/partofthething.com\/thoughts\/wp-json\/wp\/v2\/tags?post=1674"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}