Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ directory tree.

It is yet to be implemented in this library. See [issue #2](https://github.com/sjkingo/django_auth_ldap3/issues/2).

### Method 3: Bind with one user, then search and bind

The third method is used when the user's distinguished name (DN) format cannot be reliably predicted.
An user whose DN is known is used to search for the desired user's DN, which is then used to validate
her credentials. It may be seen as a subset of method 2.

Relevant settings:
* `AUTH_LDAP_BIND_AS_AUTHENTICATING_USER`: Controls whether direct binding is used
* `AUTH_LDAP_BIND_DN`: The distinguished name of the user which will search for other users
* `AUTH_LDAP_BIND_PASSWORD`: Password for the user which will search for other users
* `AUTH_LDAP_USER_DN_FILTER_TEMPLATE`: Template of the search string used to query the server. For example: `(&(objectclass=person)(uid={username}))`. It must contain `{username}` somewhere which will be substituted for the username that is being authenticated.

If using either of these settings, set `AUTH_LDAP_BIND_TEMPLATE` to `None`.

## Group membership

Sometimes it is desirable to restrict authentication to users that are members
Expand Down Expand Up @@ -275,5 +289,5 @@ Default: `True`
## Caveats

When using this library, it is strongly recommended to not manually
modify the usernames in the Django user table (either through the admin or modifying a
modify the usernames in the Django user table (either through the admin or modifying a
`User.username` field). See issues [#7](https://github.com/sjkingo/django_auth_ldap3/issues/7) and [#9](https://github.com/sjkingo/django_auth_ldap3/issues/9) for more details.
2 changes: 1 addition & 1 deletion django_auth_ldap3/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.9.6'
__version__ = '0.9.7'
139 changes: 93 additions & 46 deletions django_auth_ldap3/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from django.contrib.auth.models import Group
from django.contrib.auth import get_user_model
from ldap3.core.exceptions import LDAPSocketOpenError
import hashlib
import ldap3
import logging
Expand All @@ -12,6 +11,7 @@

logger = logging.getLogger('django_auth_ldap3')


class LDAPUser(object):
"""
A class representing an LDAP user returned from the directory.
Expand Down Expand Up @@ -40,6 +40,7 @@ def __str__(self):
def username(self):
return getattr(self, settings.UID_ATTRIB)


class LDAPBackend(object):
"""
An authentication backend for LDAP directories.
Expand Down Expand Up @@ -107,14 +108,15 @@ def authenticate(self, username=None, password=None):
if not django_user:
# Create new user. We use `ldap_user.username` here as it is the
# case-sensitive version
django_user = User(username=ldap_user.username,
password=hashlib.sha1().hexdigest(),
first_name=ldap_user.givenName,
last_name=ldap_user.sn,
email=ldap_user.mail,
is_superuser=False,
is_staff=admin,
is_active=True
django_user = User(
username=ldap_user.username,
password=hashlib.sha1().hexdigest(),
first_name=ldap_user.givenName,
last_name=ldap_user.sn,
email=ldap_user.mail,
is_superuser=False,
is_staff=admin,
is_active=True
)
django_user.save()
else:
Expand Down Expand Up @@ -158,8 +160,10 @@ def check_group_membership(self, ldap_user, group_dn):
# primaryGroupToken. This will return 0 results in OpenLDAP and hence
# be ignored.
pgt = None
group_attribs = self.search_ldap(ldap_user.connection, '(distinguishedName={})' \
.format(group_dn), attributes=['primaryGroupToken'])
group_attribs = self.search_ldap(
ldap_user.connection, '(distinguishedName={})'.format(group_dn),
attributes=['primaryGroupToken']
)
if group_attribs:
pgt = group_attribs.get('primaryGroupToken', None)
if type(pgt) == list:
Expand All @@ -168,7 +172,8 @@ def check_group_membership(self, ldap_user, group_dn):
# Now perform our group membership test. If the primary group token is not-None,
# then we wrap the filter in an OR and test for that too.
search_filter = '(&(objectClass=user)({}={})(memberof={}))'.format(
settings.UID_ATTRIB, str(ldap_user), group_dn)
settings.UID_ATTRIB, str(ldap_user), group_dn
)
if pgt:
search_filter = '(|{}(&(cn={})(primaryGroupID={})))'.format(search_filter, ldap_user.cn, pgt)

Expand Down Expand Up @@ -213,50 +218,40 @@ def bind_ldap_user(self, username, password):
"""

# Construct the user to bind as
if settings.BIND_TEMPLATE:
# Full CN
ldap_bind_user = settings.BIND_TEMPLATE.format(username=username,
base_dn=settings.BASE_DN)
elif settings.USERNAME_PREFIX:
# Prepend a prefix: useful for DOMAIN\user
ldap_bind_user = settings.USERNAME_PREFIX + username
elif settings.USERNAME_SUFFIX:
# Append a suffix: useful for user@domain
ldap_bind_user = username + settings.USERNAME_SUFFIX
logger.debug('Attempting to authenticate to LDAP by binding as ' + ldap_bind_user)
if settings.BIND_AS_AUTHENTICATING_USER:
if settings.BIND_TEMPLATE:
# Full CN
ldap_bind_user = settings.BIND_TEMPLATE.format(
username=username, base_dn=settings.BASE_DN
)
elif settings.USERNAME_PREFIX:
# Prepend a prefix: useful for DOMAIN\user
ldap_bind_user = settings.USERNAME_PREFIX + username
elif settings.USERNAME_SUFFIX:
# Append a suffix: useful for user@domain
ldap_bind_user = username + settings.USERNAME_SUFFIX
elif settings.BIND_DN and settings.BIND_PASSWORD:
ldap_bind_user = self.find_user_dn(
settings.BIND_DN, settings.BIND_PASSWORD, username=username
)

try:
c = ldap3.Connection(self.backend,
read_only=True,
lazy=False,
auto_bind=True,
client_strategy=ldap3.SYNC,
authentication=ldap3.SIMPLE,
user=ldap_bind_user,
password=password)
except ldap3.core.exceptions.LDAPSocketOpenError as e:
logger.error('LDAP connection error: ' + str(e))
logger.debug(
'Attempting to authenticate to LDAP by binding as ' + ldap_bind_user
)

conn = self._maybe_bind(ldap_bind_user, password)
if conn is None:
return None
except ldap3.core.exceptions.LDAPBindError as e:
if 'invalidCredentials' in str(e):
# Invalid bind DN or password
return None
else:
logger.error('LDAP bind error: ' + str(e))
return None
except Exception as e:
logger.exception('Caught exception when trying to connect and bind to LDAP')
raise

# Search for the user using their full DN
search_filter = '({}={})'.format(settings.UID_ATTRIB, username)
attributes = self.search_ldap(c, search_filter, attributes=LDAPUser._attrib_keys, size_limit=1)
attributes = self.search_ldap(conn, search_filter, attributes=LDAPUser._attrib_keys, size_limit=1)
if not attributes:
logger.error('LDAP search error: no results for ' + search_filter)
return None

# Construct an LDAPUser instance for this user
return LDAPUser(c, attributes)
return LDAPUser(conn, attributes)

def update_group_membership(self, ldap_user, django_user):
"""Update the user's group memberships
Expand Down Expand Up @@ -287,3 +282,55 @@ def update_group_membership(self, ldap_user, django_user):
getattr(django_user.groups, operation)(g)

django_user.save()

def find_user_dn(self, bind_user, bind_password, username=None):
conn = self._maybe_bind(bind_user, bind_password)
if conn is None:
return None

search_filter = settings.USER_DN_FILTER_TEMPLATE.format(
username=username
)
conn.search(settings.BASE_DN, search_filter)
num_results = len(conn.entries)
if num_results == 0:
logger.error('User with username {}. not found. Filter used was "{}"'.format(
username, search_filter
))
return None
elif num_results > 1:
logger.error('Error searching for username {}. {} matches found. Filter used was "{}"'.format(
username, len(num_results), search_filter
))
return None
else:
match = conn.entries[0]
return match.entry_dn

def _maybe_bind(self, user, password):
try:
conn = ldap3.Connection(
self.backend,
read_only=True,
lazy=False,
auto_bind=True,
client_strategy=ldap3.SYNC,
authentication=ldap3.SIMPLE,
user=user,
password=password
)
except ldap3.core.exceptions.LDAPSocketOpenError as e:
logger.error('LDAP connection error: ' + str(e))
return None
except ldap3.core.exceptions.LDAPBindError as e:
if 'invalidCredentials' in str(e):
# Invalid bind DN or password
return None
else:
logger.error('LDAP bind error: ' + str(e))
return None
except Exception as e:
logger.exception('Caught exception when trying to connect and bind to LDAP')
raise

return conn
5 changes: 5 additions & 0 deletions django_auth_ldap3/conf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.conf import settings as django_settings


class LDAPSettings(object):
"""
Class that provides access to the LDAP settings specified in Django's
Expand All @@ -13,7 +14,11 @@ class LDAPSettings(object):
defaults = {
'ADMIN_GROUP': None,
'BASE_DN': 'dc=example,dc=com',
'BIND_AS_AUTHENTICATING_USER': True,
'BIND_DN': '',
'BIND_PASSWORD': '',
'BIND_TEMPLATE': 'uid={username},{base_dn}',
'USER_DN_FILTER_TEMPLATE': '(&(objectclass=person)(uid={username}))',
'GROUP_MAP': None,
'LOGIN_GROUP': '*',
'UID_ATTRIB': 'uid',
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
url='https://github.com/sjkingo/django_auth_ldap3',
install_requires=[
'Django >= 1.6.10',
'ldap3 >= 0.9.7.1',
'ldap3 == 2.1.1',
],
packages=find_packages(),
classifiers=[
Expand Down