1. Peter Sagerson
  2. django-auth-ldap

Issues

Issue #21 open

Cant bind and search on ActiveDirectory when using AUTH_LDAP_BIND_AS_AUTHENTICATING_USER

Doug Napoleone
created an issue

This is a nasty corner case when dealing with how some people have set up ActiveDirectory.

This is more of a feature request than a bug, but as it means that there is no way of using django-auth-ldap with login binding I am reporting it as a bug.

Say you are trying to use AUTH_LDAP_BIND_AS_AUTHENTICATING_USER

This means you must set AUTH_LDAP_USER_DN_TEMPLATE

as you can not search until you bind.

But on many ActiveDirectory installs, the primary CN which can be bound against uses the long user name, not the login name. That is it will use something like "CN=Napoleone\5c Doug" instead of the login name 'dnapoleone'.

But the person is entering in their username, not the long name (which users rarely even know!)

This can be worked around by setting AUTH_LDAP_USER_DN_TEMPLATE = "%(user)s@DOMAIN", and binding against that.

But then this is used as the CN for all further operations, so if you are using any of the group features, then the search calls will fail.

The solution is to, after binding, use AUTH_LDAP_USER_SEARCH to search for the full CN which can then be used for future searches.

This is simple enough to fix in backend._LDAPUser:

{{{

!python

def _authenticate_user_dn(self, password):
    """
    Binds to the LDAP server with the user's DN and password. Raises
    AuthenticationFailed on failure.
    """
    if self.dn is None:
        raise self.AuthenticationFailed("Failed to map the username to a DN.")

    try:
        sticky = ldap_settings.AUTH_LDAP_BIND_AS_AUTHENTICATING_USER

        self._bind_as(self.dn, password, sticky=sticky)

        ## RED_FLAG: this is the added code, which if both
        ##           AUTH_LDAP_BIND_AS_AUTHENTICATING_USER and
        ##           AUTH_LDAP_USER_SEARCH are set, then re-populate the
        ##           user DN with the result of the search.
        if sticky and ldap_settings.AUTH_LDAP_USER_SEARCH:
            self._search_for_user_dn()

    except self.ldap.INVALID_CREDENTIALS:
        raise self.AuthenticationFailed("User DN/password rejected by LDAP server.")

}}}

Not sure when I will get around to a patch.

It might be better to have an explicit setting to use AUTH_LDAP_USER_SEARCH after binding with AUTH_LDAP_USER_DN_TEMPLATE. Maybe a AUTH_LDAP_USER_DN_SEARCH_AFTER_BIND or something. Due to the corner case I am having a hard time coming up with a good name.

Comments (17)

  1. Peter Sagerson repo owner

    I'm not quite sure I followed this, but it seems like the salient points here are:

    1. No global credentials are available for a search/bind, thus the DN must be derivable from information provided by the user, which currently means using AUTH_LDAP_USER_DN_TEMPLATE.
    2. The DN that will be used to authenticate is different from the canonical DN of the user, which will be required for retrieving attributes, finding group membership, etc.

    That second one breaks a pretty fundamental assumption, although I think your fix could perhaps be generalized. It might not be unreasonable to simply say that AUTH_LDAP_USER_DN_TEMPLATE takes precedence for authentication, but if the search/bind settings are present, we will use them to generate the user's canonical DN once we're bound. That would technically be a backwards-incompatible change, although only for configurations with currently-ignored search/bind settings.

    Let me know if I'm not understanding this. I have a gut feeling that the right way to address this will be a future 1.1 release that has a more generalized mechanism for managing multiple DNs and specifying which operations are performed using which credentials. This would probably involve an API that allows clients to furnish saved user credentials (not generally wise), which would likely be a stepping stone to a set of directory manipulation features.

  2. Doug Napoleone reporter

    Peter,

    That is correct. This is something that is specific to ActiveDirectory. Where I work we have a very old AD setup which has been upgraded many times over and is now on the latest exchange. As part of those upgrades, the ldap Schema has also changed. Older accounts can authenticate against 'uid', newer ones do not have that field, etc. The canonical authentication scheme, for direct auth and ldap login, is the string '<username>@<domain>', which is not even a DN (yea microsoft...). As it only works for authentication, it will not return or bind to the user object; again thank you microsoft.

    My change is backwards compatible, I believe, due to the current available use cases, and does not require caching any credentials. But this is taking advantage of existing implicit behavior when these configuration variables are set.

    When AUTH_LDAP_BIND_AS_AUTHENTICATING_USER is used, you must have AUTH_LDAP_USER_DN_TEMPLATE set to do a direct auth, and this is also used for the bind. The AUTH_LDAP_USER_SEARCH setting is completely ignored. That setting is not used for group lookup either, just for finding and binding the user, when AUTH_LDAP_BIND_AS_AUTHENTICATING_USER is _not_ set.

    My change takes advantage of this by using, instead of ignoring the AUTH_LDAP_USER_SEARCH setting to perform the bind, and as it is not used otherwise in this configuration, it is backwards compatible.

    There is a security risk here, if you performed an auth as someone with significant privileges, they could then bind as a different user, but that auth user would have to have such privileges in the first place; normally this would be an error. As this is essentially what you are doing when you do not use AUTH_LDAP_BIND_AS_AUTHENTICATING_USER, it really is no worse than that.

    The settings I am using are:

    AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True
    AUTH_LDAP_USER_DN_TEMPLATE = "%(user)s@DOMAIN"
    
    # does not work: Can't auth against sAMAccountName, but that is the only reliable unique field with the username.
    #AUTH_LDAP_USER_DN_TEMPLATE = "sAMAccountName=%(user)s,OU=Corp Accounts,DC=domain,DC=com"
    
    AUTH_LDAP_USER_SEARCH = LDAPSearch("DC=domain,DC=com",
        ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)")
    
    
  3. Kirill Gagarski

    I have the same problem now. In our organization LDAP wants a bind request from the authenticating user. After successful bind I want to populate some user fields from LDAP so I want to perform search.

    The sequence of LDAP requests should be as follows (let us login as thebestuser)

    bindRequest(1) "THEBESTCOMPANYEVER\thebestuser"
    bindResponse(1) success
    searchRequest(2) "OU=best_department,DC=bestcompanyever,DC=com" wholeSubtree // + search parameters: username=thebestuser
    searchResEntry(2) "cn=The Best Employee,OU=best_subsubdepartment,OU=best_subdepartment,OU=best_department" | searchResDone(2) success
    

    Is there a way to do it now? Now I am using modified version of django_auth_ldap with a patch suggested by Doug Napoleone.

  4. Ryan Allen

    Any update on this? We are already updating to Django 1.8 and would like to use AUTH_LDAP_PROFILE_ATTR_MAP. I've dug through the code and can't seem to make sense of what is trying to be accomplished. I'm happy to help contribute, just need a rundown on the situation.

  5. Doug Napoleone reporter

    here is the monkey patch we are using in all of our top level urls.py files (we are using this in 8 different django servers at work):

    from django_auth_ldap import backend
    class _LDAPUser(backend._LDAPUser):
        def _authenticate_user_dn(self, password):
            """
            Binds to the LDAP server with the user's DN and password. Raises
            AuthenticationFailed on failure.
            """
            if self.dn is None:
                raise self.AuthenticationFailed(
                    "Failed to map the username to a DN.")
    
            try:
                sticky = self.settings.BIND_AS_AUTHENTICATING_USER
    
                self._bind_as(self.dn, password, sticky=sticky)
    
                ## RED_FLAG: this is the added code, which if both
                ##           AUTH_LDAP_BIND_AS_AUTHENTICATING_USER and
                ##           AUTH_LDAP_USER_SEARCH are set, then re-populate the
                ##           user DN with the result of the search.
                if sticky and self.settings.USER_SEARCH:
                    self._search_for_user_dn()
    
            except self.ldap.INVALID_CREDENTIALS:
                raise self.AuthenticationFailed(
                    "User DN/password rejected by LDAP server.")
    backend._LDAPUser = _LDAPUser
    
  6. Jeff Blaine

    Whoa, over 4 years open on this issue!

    I've been failing for 2 hours now, trying with a bind account+password as well as direct/simple and ended up here.

    We have this same problem. Our user DNs are things like:

    CN=Blaine\, Jeff (JBLAINE),OU=USERS,OU=XY,OU=Loc,DC=EXAMPLE,DC=COM
    

    Thank you for at least sharing your monkey patch workaround, Doug. I will give it a try for now.

    Additionally, sort of an aside, something like Apache's "Require ldap-attribute" (described at https://httpd.apache.org/docs/2.2/mod/mod_authnz_ldap.html#reqattribute) would go a long way toward adding additional flexibility to django-auth-ldap.

  7. Scott Mills

    This monkey patch seems to be broken in Django 1.9. My settings.py imports ldap_fix.py at the end (the above monkey patch) and then uses the following code afterwards to import the monkey patch'd LDAPBackend:

    AUTHENTICATION_BACKENDS = [ 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend' ]

    Getting error: raise AppRegistryNotReady("Apps aren't loaded yet.") django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.

    Due to: lib\site-packages\django_auth_ldap\backend.py", line 54, in <module> from django.contrib.auth.models import User, Group, Permission

    FULL ERROR:

    File "C:\Users\Name\Documents\Projects\WM\my-api\API\WM\settings.py", line 264, in <module> from WM.ldap_fix import *

    File "C:\Users\Name\Documents\Projects\WM\my-api\API\WM\ldap_fix.py", line 3, in <module> from django_auth_ldap import backend

    File "C:\Users\Name\Envs\api\lib\site-packages\django_auth_ldap\backend.py", line 54, in <module> from django.contrib.auth.models import User, Group, Permission

    File "C:\Users\Name\Envs\api\lib\site-packages\django\contrib\auth\models.py", line 4, in <module> from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager

    File "C:\Users\Name\Envs\api\lib\site-packages\django\contrib\auth\base_user.py", line 49, in <module> class AbstractBaseUser(models.Model):

    File "C:\Users\Name\Envs\api\lib\site-packages\django\db\models\base.py", line 94, in new app_config = apps.get_containing_app_config(module)

    File "C:\Users\Name\Envs\api\lib\site-packages\django\apps\registry.py", line 239, in get_containing_app_config self.check_apps_ready()

    File "C:\Users\Name\Envs\api\lib\site-packages\django\apps\registry.py", line 124, in check_apps_ready raise AppRegistryNotReady("Apps aren't loaded yet.")

    django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.

  8. David Hoese

    Scott Mills Did you also update your version of django-auth-ldap? I'm using mezzanine with django-auth-ldap and I didn't receive the errors you did when upgrading to Django 1.9. However, I am getting a problem with User permissions not being properly set up. I've mentioned it on the Mezzanine users group https://groups.google.com/forum/#!topic/mezzanine-users/rlFFmYtEvAg if anyone thinks it might be related. I'll probably need to create an official bug report if I can't figure it out soon.

  9. Scott Mills

    I am using django-auth-ldap 1.2.7 and Django 1.9.2.

    django-auth-ldap works fine (no errors) if I don't use Doug's monkey patch - but I require it (unless anybody knows another way).

    If I revert to Django 1.8.9 I have no issues with either django-auth-ldap or the monkey patch.

    I am not seeing any user permission issues with either Django 1.8.9 or Django 1.9.2. I've edited my above post with more info.

  10. Doug Napoleone reporter

    I have not upgraded to 1.9 yet for our internal server, but Django has fundamentally changed how applications are managed in Django 1.9. You no longer directly access models or import the model modules. Instead you go through the application registry. The auth framework has also been redesigned, and django-auth-ldap is not 100% compatible with it, but as long as you are not using auth mix-ins or a backoff chain, you should be fine. Otherwise django-auth-ldap will not get the User model properly, and will not be able to make modifications to it, resulting in user permissions not properly being set. This is an independent bug which needs to be filed.

    Because of this problem I can not upgrade to Django 1.9. Once django-auth-ldap is properly ported to Django 1.9 I will be able to make a mix-in auth which will work on top of django-auth-ldap and release that instead of having an ugly monkey patch.

    The monkey patch needs to be moved to a code location which is post-app initialization or part of it. Looking at the documentation, this should be moved into a new registered app where the only thing that it does is runs this code in the body of it's ready() command. That should work, but will not fix the user permissions problems, nor the problems with interacting with other django auth backends (including the base django one).

  11. Natasha Lockhart

    Confirming that bimsapi's comment resolved this for me; it doesn't seem to matter which AppConfig object you use (I have one that's part of an app that provides user profile models, and I stuck it there), but directly copying and pasting the code from Doug Napoleone's monkey patch into any AppConfig.ready() override seems to resolve issues related populating user attributes and working with groups (in my case, using AUTH_LDAP_REQUIRE_GROUP to restrict access).

  12. Log in to comment