├── .gitignore ├── AUTHORS.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── django_auth_ldap3 ├── __init__.py ├── backends.py └── conf.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | * Sam Kingston 4 | * @gianlo 5 | * Ryan Massoth (@rmassoth) 6 | * Alan D Moore (@alandmoore) 7 | * Eduardo Castellanos (@_wayo) 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ### 0.9.6 - 2016-05-05 5 | 6 | * Python 3.5 and Django 1.9 officially supported (@sjkingo) 7 | * #19: Pass missing LDAP fields to Django as blank strings to prevent exception when creating 8 | a new user (@alandmore) 9 | 10 | ### 0.9.5 - 2016-03-30 11 | 12 | * #3: New feature `AUTH_LDAP_TLS` allows LDAP connections to be established over TLS (@_wayo) 13 | 14 | ### 0.9.4 - 2016-01-21 15 | 16 | * Use proxy method for getting `User` instance to support Django's custom user models (@alandmoore) 17 | * New feature `AUTH_LDAP_GROUP_MAP` to map LDAP groups to Django for authorization (@alandmore) 18 | 19 | ### 0.9.3 - 2015-07-06 20 | 21 | * Fix bug with case-insensitive LDAP usernames creating duplicate users in 22 | Django's auth database (@rmassoth, @sjkingo) - [issue #7](https://github.com/sjkingo/django_auth_ldap3/issues/7) 23 | 24 | ### 0.9.2 - 2015-04-27 25 | 26 | * Fix bug where primary group membership in AD would succeed regardless 27 | of actual membership (@gianlo) - PR #5 28 | 29 | ### 0.9.1 - 2015-01-14 30 | 31 | * Updated dependencies to allow Python 2.7 and Django 1.6.10 32 | * Tweaked package classifiers 33 | 34 | ### 0.9.0 - 2015-01-14 35 | 36 | * Initial working version 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, Sam Kingston , and others. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django_auth_ldap3 2 | 3 | This is a small library for connecting Django's authentication system to an 4 | LDAP directory. Unlike other similar libraries, it and its dependencies are 5 | pure-Python and do not require any special system headers to run, making it 6 | perfect for running in a hosted virtualenv. 7 | 8 | It has a sane default configuration that requires minimal customization, and 9 | has been tested against OpenLDAP and Microsoft's Active Directory. 10 | 11 | It is licensed under the [BSD license](https://github.com/sjkingo/django_auth_ldap3/blob/master/LICENSE). 12 | 13 | It is known to work with: 14 | 15 | * Python 2.7, 3.3-3.5 16 | * Django 1.6.10, 1.7-1.9 17 | 18 | Note at some point in the future, support for Python 2.7/3.3 and Django 1.6/1.7 will be dropped (see [issue #15](https://github.com/sjkingo/django_auth_ldap3/issues/15)). 19 | 20 | [![Latest Version](http://img.shields.io/pypi/v/django_auth_ldap3.svg)](https://pypi.python.org/pypi/django_auth_ldap3/) 21 | [![License](https://img.shields.io/badge/license-BSD-blue.svg)](https://github.com/sjkingo/django_auth_ldap3/blob/master/LICENSE) 22 | 23 | ## Installation 24 | 25 | The easiest way is to install from [PyPi](https://pypi.python.org/pypi/django_auth_ldap3) using pip: 26 | 27 | ``` 28 | $ pip install django_auth_ldap3 29 | ``` 30 | 31 | Alternatively you may install from the latest commit on the `master` branch: 32 | 33 | ``` 34 | $ pip install -e git+https://github.com/sjkingo/django_auth_ldap3.git#egg=django_auth_ldap3 35 | ``` 36 | 37 | ## Base configuration 38 | 39 | A full configuration reference of all settings [is available](https://github.com/sjkingo/django_auth_ldap3#configuration-reference). 40 | 41 | 1. First, add the LDAP backend to Django's `AUTHENTICATION_BACKENDS` tuple in `settings.py`: 42 | 43 | ``` 44 | AUTHENTICATION_BACKENDS = ( 45 | 'django_auth_ldap3.backends.LDAPBackend', 46 | 'django.contrib.auth.backends.ModelBackend', 47 | ) 48 | ``` 49 | 50 | We specify `ModelBackend` here as a fallback in case any superusers are defined locally in the database. 51 | 52 | 2. Point the configuration to the directory server with only two required settings: 53 | 54 | ``` 55 | AUTH_LDAP_URI = 'ldap://localhost:389' 56 | AUTH_LDAP_BASE_DN = 'dc=example,dc=com' 57 | ``` 58 | 59 | * Any valid [LDAP 60 | URI](https://www.centos.org/docs/5/html/CDS/ag/8.0/LDAP_URLs-Examples_of_LDAP_URLs.html) 61 | is allowed for the `AUTH_LDAP_URI` setting (the port is optional and will 62 | default to 389 if not specified). 63 | 64 | * TLS has been supported since [v0.9.5](https://github.com/sjkingo/django_auth_ldap3/releases/tag/v0.9.5) with `AUTH_LDAP_TLS`. 65 | 66 | * `AUTH_LDAP_BASE_DN` must be set to the base container to perform any subtree 67 | searches from. 68 | 69 | ## Configuration for authenticating 70 | 71 | There are two methods of authenticating against an LDAP directory. 72 | 73 | ### Method 1: Direct binding 74 | 75 | This is by far the easiest method to use, and requires minimal configuration. 76 | In this method, the username and password provided during authentication are 77 | used to bind directly to the directory. If the bind fails, the 78 | username/password combination (or `AUTH_LDAP_BIND_TEMPLATE` [1]) is incorrect. 79 | 80 | [1] When direct binding, there is no way to distinguish between an incorrect 81 | username/password and the bind template being incorrect, since both result in 82 | an invalid bind. 83 | 84 | Only one extra setting is required for direct binding to an OpenLDAP directory: 85 | 86 | * `AUTH_LDAP_BIND_TEMPLATE`: the template to use when constructing the user to bind. For example: `uid={username},ou=People`. It must contain `{username}` somewhere which will be substituted for the username that is being authenticated. 87 | 88 | Alternatively you may wish to change the attribute that matches the Django username - by default it is `uid`: 89 | 90 | * `AUTH_LDAP_UID_ATTRIB`: the attribute used for a unique username (e.g. `uid` or `sAMAccountName`) 91 | 92 | The key requirement for direct binding is that a distinguished name is able to 93 | be constructed from a given username, for instance: 94 | 95 | * username `'jsmith'` is known with a distinguished name of `'uid=jsmith,ou=People,dc=example,dc=com'` in the directory 96 | 97 | A point to note here is that if you are using Active Directory, you may tell 98 | the backend to bind with a full user principal instead, such as `DOMAIN\user` 99 | or `user@domain`. This can be accomplished by setting one of the following 100 | settings: 101 | 102 | * `AUTH_LDAP_USERNAME_PREFIX`: e.g. `DOMAIN\` 103 | * `AUTH_LDAP_USERNAME_SUFFIX`: e.g. `@domain.com` 104 | 105 | If using either of these settings, set `AUTH_LDAP_BIND_TEMPLATE` to `None`. You 106 | will almost certainly want to change the `AUTH_LDAP_UID_ATTRIB` to 107 | `sAMAccountName`. 108 | 109 | ### Method 2: Search and bind 110 | 111 | The second method is more flexible but requires more directory-specific 112 | configuration: it allows you to filter users by any valid LDAP filter, across a 113 | directory tree. 114 | 115 | It is yet to be implemented in this library. See [issue #2](https://github.com/sjkingo/django_auth_ldap3/issues/2). 116 | 117 | ## Group membership 118 | 119 | Sometimes it is desirable to restrict authentication to users that are members 120 | of a specific LDAP group. This may be accomplished by setting the 121 | `AUTH_LDAP_LOGIN_GROUP` setting. By default it is set to `'*'`; any valid user 122 | may authenticate. If you wish to restrict this, change the setting to the 123 | distinguished name of a group, for example: 124 | 125 | ``` 126 | AUTH_LDAP_LOGIN_GROUP = 'cn=Web Users,ou=Groups,dc=example,dc=com' 127 | ``` 128 | 129 | You may also allow a subset of users to authenticate to the Django admin 130 | interface by setting the `AUTH_LDAP_ADMIN_GROUP` setting. By default this is 131 | set to `None`, indicating no user may have access to the admin. If you wish to 132 | allow access, change the setting to the distinguished name of a group, for 133 | example: 134 | 135 | ``` 136 | AUTH_LDAP_ADMIN_GROUP = 'cn=Admin Users,ou=Groups,dc=example,dc=com' 137 | ``` 138 | 139 | Should you wish to map LDAP groups to Django groups, you can use the `AUTH_LDAP_GROUP_MAP` 140 | setting. By default it is set to `None`, indicating that no mapping will occur. The mapping is 141 | done in the form of a dict where the keys are LDAP group DNs and the values are sequences of Django groups, 142 | for example: 143 | 144 | ``` 145 | AUTH_LDAP_GROUP_MAP = { 146 | 'cn=Admin Users,ou=Groups,dc=example,dc=com': ('site_admins', 'editors'), 147 | 'cn=Authors,ou=Groups,dc=example,dc=com': ('editors',) 148 | } 149 | ``` 150 | 151 | Note that any Django groups you list will be controlled by this mapping, and can't be manually managed, 152 | because users will be added or removed from the groups according to their LDAP group memberships at login. 153 | Any Django groups not included in the mappings will be unaffected. 154 | 155 | ## Example configuration for OpenLDAP 156 | 157 | ``` 158 | AUTH_LDAP_URI = 'ldap://localhost:389' 159 | AUTH_LDAP_BASE_DN = 'ou=People,dc=example,dc=com' 160 | AUTH_LDAP_BIND_TEMPLATE = 'uid={username},{base_dn}' 161 | ``` 162 | 163 | The last line is only required if the bind template differs from the default. 164 | 165 | ## Example configuration for Active Directory 166 | 167 | ``` 168 | AUTH_LDAP_URI = 'ldap://DC1.example.com:389' 169 | AUTH_LDAP_BASE_DN = 'dc=example,dc=com' 170 | AUTH_LDAP_BIND_TEMPLATE = None 171 | AUTH_LDAP_USERNAME_PREFIX = 'DOMAIN\\' 172 | AUTH_LDAP_UID_ATTRIB = 'sAMAccountName' 173 | ``` 174 | 175 | ## Configuration reference 176 | 177 | #### `AUTH_LDAP_BASE_DN` 178 | 179 | Default: `'dc=example,dc=com'` 180 | 181 | **Required.** The base container to perform any subtree searches from. 182 | 183 | #### `AUTH_LDAP_BIND_TEMPLATE` 184 | 185 | Default: `'uid={username},{base_dn}'` 186 | 187 | **Required.** Template used to construct the distinguished name of the user to authenticate. 188 | 189 | Valid substitution specifiers are: 190 | 191 | * `{username}` (required): the username being authenticated 192 | * `{base_dn}`: will be substituted for `AUTH_LDAP_BASE_DN` 193 | 194 | #### `AUTH_LDAP_URI` 195 | 196 | Default: `'ldap://localhost'` 197 | 198 | **Required.** A valid LDAP URI that specifies a connection to a directory server. 199 | 200 | TLS has been supported since [v0.9.5](https://github.com/sjkingo/django_auth_ldap3/releases/tag/v0.9.5) with `AUTH_LDAP_TLS`. 201 | 202 | #### `AUTH_LDAP_ADMIN_GROUP` 203 | 204 | Default: `None` 205 | 206 | *Optional.* Distinguished name of the group of users allowed to access the admin area, or `None` 207 | to deny all. 208 | 209 | #### `AUTH_LDAP_GROUP_MAP` 210 | 211 | Default: `None` 212 | 213 | *Optional.* Dictionary of LDAP groups to Django groups to perform mapping on. 214 | See *Group membership*, above, for more details. 215 | 216 | *Added in version 0.9.4* 217 | 218 | #### `AUTH_LDAP_LOGIN_GROUP` 219 | 220 | Default: `'*'` 221 | 222 | *Optional.* Restrict authentication to users that are a member of this group 223 | (distinguished name). `'*'` indicates any user may authenticate. 224 | 225 | #### `AUTH_LDAP_UID_ATTRIB` 226 | 227 | Default: `'uid'` 228 | 229 | *Optional.* The unique attribute in the directory that stores the username. For 230 | Active Directory this will be `sAMAccountName`. 231 | 232 | #### `AUTH_LDAP_USERNAME_PREFIX` 233 | 234 | Default: `None` 235 | 236 | *Optional.* String to prefix the username before binding. This is used for `DOMAIN\user` principals. 237 | 238 | You must set `AUTH_LDAP_BIND_TEMPLATE` to `None` when using this option. 239 | 240 | #### `AUTH_LDAP_USERNAME_SUFFIX` 241 | 242 | Default: `None` 243 | 244 | *Optional.* String to suffix the username before binding. This is used for `user@domain` principals. 245 | 246 | You must set `AUTH_LDAP_BIND_TEMPLATE` to `None` when using this option. 247 | 248 | *Added in version 0.9.5* 249 | 250 | #### `AUTH_LDAP_TLS` 251 | 252 | *Optional.* Flag to enable LDAP over TLS. Further options can be configured through `AUTH_LDAP_TLS_CA_CERTS`, 253 | `AUTH_LDAP_TLS_VALIDATE`, `AUTH_LDAP_TLS_PRIVATE_KEY`, and `AUTH_LDAP_TLS_LOCAL_CERT`. 254 | 255 | Default: `False` 256 | 257 | #### `AUTH_LDAP_TLS_CA_CERTS` 258 | *Optional.* String to the location of the file containing the certificates of the certification authorities. 259 | 260 | It's checked only if `AUTH_LDAP_TLS_VALIDATE` is set to `True`. 261 | 262 | Default: It will use the system wide certificate store. 263 | 264 | #### `AUTH_LDAP_TLS_VALIDATE` 265 | *Optional.* Specifies if the server certificate must be validated. 266 | 267 | Default: `True` 268 | 269 | #### `AUTH_LDAP_TLS_PRIVATE_KEY` 270 | *Optional.* Specifies the location for the file with the private key of the client. 271 | 272 | #### `AUTH_LDAP_TLS_LOCAL_CERT` 273 | *Optional.* Specifies the location for the file with the certificate of the server. 274 | 275 | ## Caveats 276 | 277 | When using this library, it is strongly recommended to not manually 278 | modify the usernames in the Django user table (either through the admin or modifying a 279 | `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. 280 | -------------------------------------------------------------------------------- /django_auth_ldap3/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.9.6' 2 | -------------------------------------------------------------------------------- /django_auth_ldap3/backends.py: -------------------------------------------------------------------------------- 1 | from django_auth_ldap3.conf import settings 2 | 3 | from django.contrib.auth.models import Group 4 | from django.contrib.auth import get_user_model 5 | from ldap3.core.exceptions import LDAPSocketOpenError 6 | import hashlib 7 | import ldap3 8 | import logging 9 | import ssl 10 | 11 | User = get_user_model() 12 | 13 | logger = logging.getLogger('django_auth_ldap3') 14 | 15 | class LDAPUser(object): 16 | """ 17 | A class representing an LDAP user returned from the directory. 18 | """ 19 | 20 | connection = None 21 | _attrib_keys = [settings.UID_ATTRIB, 'cn', 'givenName', 'sn', 'mail'] 22 | 23 | def __init__(self, connection, attributes): 24 | self.connection = connection 25 | for k, v in attributes.items(): 26 | # Flatten any lists into their first element 27 | if type(v) == list and len(v) >= 1: 28 | v = v[0] 29 | setattr(self, k, v) 30 | 31 | # Set any missing attributes 32 | for k in self._attrib_keys: 33 | if not hasattr(self, k): 34 | setattr(self, k, '') 35 | 36 | def __str__(self): 37 | return self.username 38 | 39 | @property 40 | def username(self): 41 | return getattr(self, settings.UID_ATTRIB) 42 | 43 | class LDAPBackend(object): 44 | """ 45 | An authentication backend for LDAP directories. 46 | """ 47 | 48 | backend = None 49 | 50 | def __init__(self): 51 | tls_config = None 52 | 53 | if settings.TLS: 54 | tls_opts = { 55 | 'validate': ssl.CERT_REQUIRED if settings.TLS_VALIDATE else ssl.CERT_NONE 56 | } 57 | 58 | if settings.TLS_CA_CERTS: 59 | tls_opts['ca_certs_file'] = settings.TLS_CA_CERTS 60 | 61 | if settings.TLS_PRIVATE_KEY: 62 | tls_opts['local_private_key_file'] = settings.TLS_PRIVATE_KEY 63 | 64 | if settings.TLS_LOCAL_CERT: 65 | tls_opts['local_certificate_file'] = settings.TLS_LOCAL_CERT 66 | 67 | tls_config = ldap3.Tls(**tls_opts) 68 | 69 | self.backend = ldap3.Server(settings.URI, use_ssl=settings.TLS, tls=tls_config) 70 | 71 | def __del__(self): 72 | # TODO: disconnect? 73 | pass 74 | 75 | def authenticate(self, username=None, password=None): 76 | """ 77 | Required for Django auth. Authenticate the uesr against the LDAP 78 | backend and populate the local User model if this is the first 79 | time the user has authenticated. 80 | """ 81 | 82 | # Authenticate against the LDAP backend and return an LDAPUser. 83 | ldap_user = self.retrieve_ldap_user(username, password) 84 | if ldap_user is None: 85 | logger.debug('Authentication failed for {}'.format(ldap_user)) 86 | return None 87 | 88 | # If we get here, authentication is successful and we have an LDAPUser 89 | # instance populated with the user's attributes. We still need to check 90 | # group membership and populate a local User model. 91 | 92 | # Check LDAP group membership before creating a local user. The default 93 | # is '*' so any user can log in. 94 | if not self.check_group_membership(ldap_user, settings.LOGIN_GROUP): 95 | logger.debug('Failed group membership test: {} !memberOf {}'.format(ldap_user, settings.LOGIN_GROUP)) 96 | return None 97 | 98 | # Check if this user is part of the admin group. 99 | admin = False 100 | if settings.ADMIN_GROUP: 101 | admin = self.check_group_membership(ldap_user, settings.ADMIN_GROUP) 102 | 103 | # Get or create the User object in Django's auth, populating it with 104 | # fields from the LDAPUser. Note we set the password to a random hash 105 | # as authentication should never occur directly off this user. 106 | django_user = User.objects.filter(username__iexact=username) 107 | if not django_user: 108 | # Create new user. We use `ldap_user.username` here as it is the 109 | # case-sensitive version 110 | django_user = User(username=ldap_user.username, 111 | password=hashlib.sha1().hexdigest(), 112 | first_name=ldap_user.givenName, 113 | last_name=ldap_user.sn, 114 | email=ldap_user.mail, 115 | is_superuser=False, 116 | is_staff=admin, 117 | is_active=True 118 | ) 119 | django_user.save() 120 | else: 121 | # If the user wasn't created, update its fields from the directory. 122 | django_user = django_user[0] 123 | django_user.first_name = ldap_user.givenName 124 | django_user.last_name = ldap_user.sn 125 | django_user.email = ldap_user.mail 126 | django_user.is_staff = admin 127 | django_user.save() 128 | 129 | self.update_group_membership(ldap_user, django_user) 130 | 131 | return django_user 132 | 133 | def get_user(self, user_id): 134 | """ 135 | Required for Django auth. 136 | """ 137 | try: 138 | return User.objects.get(pk=user_id) 139 | except User.DoesNotExist: 140 | return None 141 | 142 | def check_group_membership(self, ldap_user, group_dn): 143 | """ 144 | Check the LDAP user to see if it is a member of the given group. 145 | 146 | This is straightforward with OpenLDAP but tricky with AD as due to 147 | the weird way AD handles "primary" group membership, we must test for 148 | a separate attribute as well as the usual 'memberof' as the primary 149 | group is not returned with that filter. 150 | """ 151 | 152 | # Don't bother search directory if '*' was given: this denotes any group 153 | # so pass the test immediately. 154 | if group_dn == '*': 155 | return True 156 | 157 | # Hack for AD: fetch the group's attributes and check for the 158 | # primaryGroupToken. This will return 0 results in OpenLDAP and hence 159 | # be ignored. 160 | pgt = None 161 | group_attribs = self.search_ldap(ldap_user.connection, '(distinguishedName={})' \ 162 | .format(group_dn), attributes=['primaryGroupToken']) 163 | if group_attribs: 164 | pgt = group_attribs.get('primaryGroupToken', None) 165 | if type(pgt) == list: 166 | pgt = pgt[0] 167 | 168 | # Now perform our group membership test. If the primary group token is not-None, 169 | # then we wrap the filter in an OR and test for that too. 170 | search_filter = '(&(objectClass=user)({}={})(memberof={}))'.format( 171 | settings.UID_ATTRIB, str(ldap_user), group_dn) 172 | if pgt: 173 | search_filter = '(|{}(&(cn={})(primaryGroupID={})))'.format(search_filter, ldap_user.cn, pgt) 174 | 175 | # Return True if user is a member of group 176 | r = self.search_ldap(ldap_user.connection, search_filter) 177 | return r is not None 178 | 179 | def retrieve_ldap_user(self, username, password): 180 | """ 181 | Proxy method for retrieving an LDAP user depending on configuration. 182 | Currently we only support direct binding. 183 | """ 184 | return self.bind_ldap_user(username, password) 185 | 186 | def search_ldap(self, connection, ldap_filter, **kwargs): 187 | """ 188 | Searches the LDAP directory against the given LDAP filter in the form 189 | of '(attr=val)' e.g. '(uid=test)'. 190 | 191 | Any keyword arguments will be passed directly to the underlying search 192 | method. 193 | 194 | A dictionary of attributes will be returned. 195 | """ 196 | connection.search(settings.BASE_DN, ldap_filter, **kwargs) 197 | entry = None 198 | for d in connection.response: 199 | if d['type'] == 'searchResEntry': 200 | entry = d['attributes'] 201 | entry['dn'] = d['dn'] 202 | break 203 | return entry 204 | 205 | def bind_ldap_user(self, username, password): 206 | """ 207 | Attempts to bind the specified username and password and returns 208 | an LDAPUser object representing the user. 209 | 210 | Returns None if the bind was unsuccessful. 211 | 212 | This implements direct binding. 213 | """ 214 | 215 | # Construct the user to bind as 216 | if settings.BIND_TEMPLATE: 217 | # Full CN 218 | ldap_bind_user = settings.BIND_TEMPLATE.format(username=username, 219 | base_dn=settings.BASE_DN) 220 | elif settings.USERNAME_PREFIX: 221 | # Prepend a prefix: useful for DOMAIN\user 222 | ldap_bind_user = settings.USERNAME_PREFIX + username 223 | elif settings.USERNAME_SUFFIX: 224 | # Append a suffix: useful for user@domain 225 | ldap_bind_user = username + settings.USERNAME_SUFFIX 226 | logger.debug('Attempting to authenticate to LDAP by binding as ' + ldap_bind_user) 227 | 228 | try: 229 | c = ldap3.Connection(self.backend, 230 | read_only=True, 231 | lazy=False, 232 | auto_bind=True, 233 | client_strategy=ldap3.SYNC, 234 | authentication=ldap3.SIMPLE, 235 | user=ldap_bind_user, 236 | password=password) 237 | except ldap3.core.exceptions.LDAPSocketOpenError as e: 238 | logger.error('LDAP connection error: ' + str(e)) 239 | return None 240 | except ldap3.core.exceptions.LDAPBindError as e: 241 | if 'invalidCredentials' in str(e): 242 | # Invalid bind DN or password 243 | return None 244 | else: 245 | logger.error('LDAP bind error: ' + str(e)) 246 | return None 247 | except Exception as e: 248 | logger.exception('Caught exception when trying to connect and bind to LDAP') 249 | raise 250 | 251 | # Search for the user using their full DN 252 | search_filter = '({}={})'.format(settings.UID_ATTRIB, username) 253 | attributes = self.search_ldap(c, search_filter, attributes=LDAPUser._attrib_keys, size_limit=1) 254 | if not attributes: 255 | logger.error('LDAP search error: no results for ' + search_filter) 256 | return None 257 | 258 | # Construct an LDAPUser instance for this user 259 | return LDAPUser(c, attributes) 260 | 261 | def update_group_membership(self, ldap_user, django_user): 262 | """Update the user's group memberships 263 | 264 | Checks settings.GROUP_MAP to determine group memberships 265 | that should be added. 266 | """ 267 | 268 | if not settings.GROUP_MAP: 269 | return None 270 | 271 | groups = {'add': [], 'remove': []} 272 | for ldap_group, django_groups in settings.GROUP_MAP.items(): 273 | if self.check_group_membership(ldap_user, ldap_group): 274 | groups['add'] += [group for group in django_groups if group not in groups['add']] 275 | else: 276 | groups['remove'] += [group for group in django_groups if group not in groups['remove']] 277 | 278 | for operation in ('remove', 'add'): 279 | grouplist = groups[operation] 280 | for group in grouplist: 281 | try: 282 | g = Group.objects.get(name=group) 283 | except Group.DoesNotExist: 284 | logger.error('Django group does not exist: {}'.format(group)) 285 | continue 286 | else: 287 | getattr(django_user.groups, operation)(g) 288 | 289 | django_user.save() 290 | -------------------------------------------------------------------------------- /django_auth_ldap3/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings as django_settings 2 | 3 | class LDAPSettings(object): 4 | """ 5 | Class that provides access to the LDAP settings specified in Django's 6 | settings, with defaults set if they are missing. 7 | 8 | Settings are prefixed in Django's settings, but are used here without prefix. 9 | So `AUTH_LDAP_URI` becomes `settings.URI`. 10 | """ 11 | 12 | prefix = 'AUTH_LDAP_' 13 | defaults = { 14 | 'ADMIN_GROUP': None, 15 | 'BASE_DN': 'dc=example,dc=com', 16 | 'BIND_TEMPLATE': 'uid={username},{base_dn}', 17 | 'GROUP_MAP': None, 18 | 'LOGIN_GROUP': '*', 19 | 'UID_ATTRIB': 'uid', 20 | 'USERNAME_PREFIX': None, 21 | 'USERNAME_SUFFIX': None, 22 | 'URI': 'ldap://localhost', 23 | 'TLS': False, 24 | 'TLS_CA_CERTS': None, 25 | 'TLS_VALIDATE': True, 26 | 'TLS_PRIVATE_KEY': None, 27 | 'TLS_LOCAL_CERT': None, 28 | } 29 | 30 | def __init__(self): 31 | for name, default in self.defaults.items(): 32 | v = getattr(django_settings, self.prefix + name, default) 33 | setattr(self, name, v) 34 | 35 | @property 36 | def settings_dict(self): 37 | return {k: getattr(self, k) for k in self.defaults.keys()} 38 | 39 | settings = LDAPSettings() 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | from django_auth_ldap3 import __version__ as version 4 | 5 | setup( 6 | name='django_auth_ldap3', 7 | version=version, 8 | license='BSD', 9 | author='Sam Kingston', 10 | author_email='sam@sjkwi.com.au', 11 | description='A library for connecting Django\'s authentication system to an LDAP directory', 12 | url='https://github.com/sjkingo/django_auth_ldap3', 13 | install_requires=[ 14 | 'Django >= 1.6.10', 15 | 'ldap3 >= 0.9.7.1', 16 | ], 17 | packages=find_packages(), 18 | classifiers=[ 19 | 'Development Status :: 5 - Production/Stable', 20 | 'Environment :: Web Environment', 21 | 'Framework :: Django', 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: BSD License', 24 | 'Operating System :: OS Independent', 25 | 'Programming Language :: Python', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3.3', 28 | 'Programming Language :: Python :: 3.4', 29 | 'Programming Language :: Python :: 3.5', 30 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 31 | 'Topic :: Software Development :: Libraries :: Python Modules', 32 | 'Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP', 33 | ], 34 | ) 35 | --------------------------------------------------------------------------------