Guddu
Guddu

Reputation: 1588

Django Auth LDAP - Direct Bind using sAMAccountName

There are two ways to authenticate a user using Django Auth LDAP

  1. Search/Bind and
  2. Direct Bind.

The first one involves connecting to the LDAP server either anonymously or with a fixed account and searching for the distinguished name of the authenticating user. Then we can attempt to bind again with the user’s password.

The second method is to derive the user’s DN from his username and attempt to bind as the user directly.

I want to be able to do a direct bind using the userid (sAMAccountName) and password of the user who is trying to gain access to the application. Please let me know if there is a way to achieve this? At the moment, I cannot seem to make this work due to the problem explained below.

In my case, the DN of users in LDAP is of the following format

**'CN=Steven Jones,OU=Users,OU=Central,OU=US,DC=client,DC=corp'**

This basically translates to 'CN=FirstName LastName,OU=Users,OU=Central,OU=US,DC=client,DC=corp'

This is preventing me from using Direct Bind as the sAMAccountName of the user is sjones and this is the parameter that corresponds to the user name (%user) and I can't figure out a way to frame a proper AUTH_LDAP_USER_DN_TEMPLATE to derive the User's DN using.

Due to the above explained problem, I am using Search/Bind for now but this requires me to have a fixed user credential to be specified in AUTH_LDAP_BIND_DN and AUTH_LDAP_BIND_PASSWORD.

Here is my current settings.py configuration

AUTH_LDAP_SERVER_URI = "ldap://10.5.120.161:389"
AUTH_LDAP_BIND_DN='CN=Steven Jones,OU=Users,OU=Central,OU=US,DC=client,DC=corp'
AUTH_LDAP_BIND_PASSWORD='fga.1234'
#AUTH_LDAP_USER_DN_TEMPLATE = 'CN=%(user)s,OU=Appl Groups,OU=Central,OU=US,DC=client,DC=corp'
AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(
    LDAPSearch("OU=Users, OU=Central,OU=US,DC=client,DC=corp",ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"),
    LDAPSearch("OU=Users,OU=Regional,OU=Locales,OU=US,DC=client,DC=corp",ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"),
    )
AUTH_LDAP_USER_ATTR_MAP = {"first_name": "givenName", "last_name": "sn","email":"mail"}
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("CN=GG_BusinessApp_US,OU=Appl Groups,OU=Central,OU=US,DC=client,DC=corp",ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)")
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
AUTH_LDAP_REQUIRE_GROUP = 'CN=GG_BusinessApp_US,OU=Appl Groups,OU=Central,OU=US,DC=client,DC=corp'

Looking forward for some guidance from the wonderful folks in here.

Upvotes: 9

Views: 18828

Answers (7)

schnurrel
schnurrel

Reputation: 23

I struggled with the same problem, but (at least with my setup) I found out that the solution is not that difficult and doesn't even required any monkey patching.

TLDR: I got it to work and my very basic settings looked like this:

AUTH_LDAP_SERVER_URI = "ldap://internal.company.com"
AUTH_LDAP_USER_DN_TEMPLATE = '%(user)[email protected]'  # alternatively you might get something like  r'company\%(user)s' to work
AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True  # this is probably not necessary, since there is not initial bind to perform a search

That is enough to let me authenticate with my user handle, which is stored in the sAMAccountName in our active directory. I later wanted to add user attribute mapping, so I had to refresh the bind after authentication. My settings.py now looks like this:

AUTH_LDAP_SERVER_URI = "ldap://internal.company.com"
AUTH_LDAP_USER_DN_TEMPLATE = r'company\%(user)s' # use domain structure to allow login via sAMAccountname, ...
AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True     # ... then bind as the authenticated user so ...
AUTH_LDAP_REFRESH_DN_ON_BIND = True              # ... the authenticated user dn can be refreshed using ...
AUTH_LDAP_USER_SEARCH = LDAPSearch(              # ... an ldap search in the users base DN
    "OU=Users,DC=internal,DC=company,DC=com",
    ldap.SCOPE_SUBTREE,
    "(sAMAccountName=%(user)s)"
)

# Then set AUTH_LDAP_GROUP_SEARCH, AUTH_LDAP_GROUP_TYPE, AUTH_LDAP_USER_ATTR_MAP
# ...

More explanation:
In my opinion, the naming AUTH_LDAP_USER_DN_TEMPLATE is a little misleading (the answer of @jan-staal pointed me towards this) as it doesn't necessarily require the distinguished name (DN). The module django-auth-ldap calls python-ldaps LDAPObject.simple_bind_s(username, pw) function, where whatever you provide in AUTH_LDAP_USER_DN_TEMPLATE is used as the username, and that accepts several shortcuts for authentication. I set up a simple test connection with python-ldap in a jupyter notebook like this:

import ldap
str_ldap_server = "ldap://internal.company.com"
# use sAMAccountName and the ldap uri
username = "[email protected]"  
# alternatively use the company domain name and sAMAccountName
# username = r"company\sjones"
pw = "topsecret"
conn = ldap.initialize(str_ldap_server)
conn.simple_bind_s(username, pw)
conn.whoami_s()

Both username options returned me u:company\\sjones, confirming a successful connection

Important: the first variant of providing the username is not necessarily equivalent to your ldap's userPrincipalName! In my example my userPrincipalName is [email protected], but the bind required [email protected] (to match the ldap url)

Anyhow, with the connection test I found a username structure that worked, which I could use for AUTH_LDAP_USER_DN_TEMPLATE. Then the above settings also worked for me in django. I hopes this can be of help to someone. :)

My setup is

  • Django==4.2.6
  • django-auth-ldap==4.8.0 with
  • python-ldap==3.4.4

Upvotes: 0

Jan Staal
Jan Staal

Reputation: 41

The above answers did not work for me, but I have found a way to make it work. The trick is to use sAMAAcountname in combination with the domain name to bind.

  1. modify the template DN in order for it to use [email protected] format.
  2. use modified monkey patch to lookup and store the real user CN (self._user_dn).

Settings:

AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True
AUTH_LDAP_USER_DN_TEMPLATE = '%(user)[email protected]'

Patch:

import ldap
from django_auth_ldap import backend

def monkey(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)

    # Search for the user DN -->
    if sticky and self.settings.USER_SEARCH:
        self._user_dn = self._search_for_user_dn()

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

backend._LDAPUser._authenticate_user_dn = monkey

Upvotes: 1

amethystdragon
amethystdragon

Reputation: 235

I had the same issue.

I ran across ticket 21 in the now-deleted bitbucket repository. (cant-bind-and-search-on-activedirectory). The issues were not migrated to their github, but the author brought up a way to change the library files for django-auth-ldap so that it could do a direct bind.

It came down to changing <python library path>/django_auth_ldap/backend.py to include two lines in _authenticate_user_dn:

if sticky and ldap_settings.AUTH_LDAP_USER_SEARCH:
    self._search_for_user_dn()

I was able to get this to work on my local machine that was running Arch Linux 3.9.8-1-ARCH, but I was unable to replicate it on the dev server running Ubuntu 13.04.

Hopefully this can help.

Upvotes: 10

bechir nahali
bechir nahali

Reputation: 11

I think using direct bind (as shown below) and then passing the common name in the login interface would do the job, and thus it is not needed to set static authentication credentials.

AUTH_LDAP_USER_DN_TEMPLATE = "CN=%(user)s,OU=users,OU=OR-TN,DC=OrangeTunisie,DC=intra"

Upvotes: 0

safety dance
safety dance

Reputation: 11

I also had this issue, but I didn't want to modify the settings.py file. The fix for me was to comment out the line "AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=path,dc=to,dc=domain"". I also added NestedActiveDirectoryGroupType as part of my troubleshooting. Not sure if it's necessary, but it's working now so I'm leaving it. Here's my ldap_config.py file.

import ldap

# Server URI
AUTH_LDAP_SERVER_URI = "ldap://urlForLdap"

# The following may be needed if you are binding to Active Directory.
AUTH_LDAP_CONNECTION_OPTIONS = {
       # ldap.OPT_DEBUG_LEVEL: 1,
    ldap.OPT_REFERRALS: 0
}

# Set the DN and password for the NetBox service account.
AUTH_LDAP_BIND_DN = "CN=Netbox,OU=xxx,DC=xxx,DC=xxx"
AUTH_LDAP_BIND_PASSWORD = "password"

# Include this setting if you want to ignore certificate errors. This might be needed to accept a self-signed cert.
# Note that this is a NetBox-specific setting which sets:
#     ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
LDAP_IGNORE_CERT_ERRORS = True

from django_auth_ldap.config import LDAPSearch, NestedActiveDirectoryGroupType

# This search matches users with the sAMAccountName equal to the provided username. This is required if the user's
# username is not in their DN (Active Directory).
AUTH_LDAP_USER_SEARCH = LDAPSearch("OU=xxx,DC=xxx,DC=xxx",
                                    ldap.SCOPE_SUBTREE,
                                    "(sAMAccountName=%(user)s)")

# If a user's DN is producible from their username, we don't need to search.
# AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=corp,dc=loc"

# You can map user attributes to Django attributes as so.
AUTH_LDAP_USER_ATTR_MAP = {
    "first_name": "givenName",
    "last_name": "sn",
    "email": "mail"
}

from django_auth_ldap.config import LDAPSearch, GroupOfNamesType, NestedActiveDirectoryGroupType

# This search ought to return all groups to which the user belongs. django_auth_ldap uses this to determine group
# heirarchy.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("dc=xxx,dc=xxx", ldap.SCOPE_SUBTREE,
                                    "(objectClass=group)")
AUTH_LDAP_GROUP_TYPE = NestedActiveDirectoryGroupType()

# Define a group required to login.
AUTH_LDAP_REQUIRE_GROUP = "CN=NetBox_Users,OU=NetBox,OU=xxx,DC=xxx,DC=xxx"

# Define special user types using groups. Exercise great caution when assigning superuser status.
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
    "is_active": "CN=NetBox_Active,OU=NetBox,OU=xxx,DC=xxx,DC=xxx",
    "is_staff": "CN=NetBox_Staff,OU=NetBox,OU=xxx,DC=xxx,DC=xxx",
    "is_superuser": "CN=NetBox_Superuser,OU=NetBox,OU=xxx,DC=xxx,DC=xxx"
}

# For more granular permissions, we can map LDAP groups to Django groups.
AUTH_LDAP_FIND_GROUP_PERMS = True

# Cache groups for one hour to reduce LDAP traffic
AUTH_LDAP_CACHE_GROUPS = True
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600

Upvotes: 1

Mika R
Mika R

Reputation: 105

I also had this problem where the old ldap server had a dn that started with uid, but the DN of the new one starts with CN ('Steven Jones'). I used this configuration (which solved it for me) in setting.py:

AUTH_LDAP_BIND_DN = 'CN=adreader,CN=Users,DC=xxx, DC=yyy'

from django_auth_ldap.config import LDAPSearch
import ldap
AUTH_LDAP_USER_SEARCH = LDAPSearch(base_dn='ou=People, ou=xxx, dc=yyy, dc=zzz, 
  scope=ldap.SCOPE_SUBTREE, filterstr='(sAMAccountName=%(user)s)')

Upvotes: 0

tuomassalo
tuomassalo

Reputation: 9101

(This is actually a comment to @amethystdragon's answer, but it's a bunch of code, so posting as a separate answer.) The problem still seems to exist with django_auth_ldap 1.2.5. Here's an updated patch. If you don't want or can't modify the source code, monkey-patching is possible. Just put this code to eg. end of settings.py. (And yes, I know monkey-patching is ugly.)

import ldap
from django_auth_ldap import backend

def monkey(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)

    #### The fix -->
    if sticky and self.settings.USER_SEARCH:
      self._search_for_user_dn()
    #### <-- The fix

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

backend._LDAPUser._authenticate_user_dn = monkey

Upvotes: 5

Related Questions