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 (8)

  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. Log in to comment