considering that I did not find any working auth module for django able to work with MS Active Directory as a LDAP Server, I started to build my own.
Once configured as described bellow, the module authenticates users against a single or a pool of LDAP servers and tries reconciling the user with the local django database. If user does not exists locally, it creates it, else it updates it. Moreover, it tries to determine group membership following the rules given in settings.py
pip install django-auth-ldap3-ad
Mandatory parameter. List of LDAP server to authenticate against. 3 information are needed for each:
- it's name or IP (prefer IP to avoid spending time with the DNS lookup)
- the TCP port to connect on (default: 389 for non SSL and 636 for SSL)
- a boolean tag to enable or not the use of SSL during authentication process against this server.
You can define as many authentication servers as needed (as many as you have in you network) with, for each, it's own parameters. The pool is used in the order you defined. If the first one is available, authentication is against it, if the server is not available, the next is used. In all cases, if an answer is received for a server, it's considered authoritative even if it's negative
LDAP_SERVERS = [
{
'host': '<server 1 IP>',
'port': 389,
'use_ssl': False,
},
{
'host': '<server 2 IP>',
'port': 389,
'use_ssl': False,
},
]
As some capabilities differs between LDAP engines, you can define your engine to use specific features.
Actual used values are:
- AD (default)
- OpenLDAP
LDAP_ENGINE = 'OpenLDAP'
Mandatory parameter. To bind to a LDAP server, we need a fully qualified distinguished name. The user just needs minimal rights to walk through the directory. That's why it is recommended to create a dedicated user with no special rights and no groups to use here.
LDAP_BIND_USER = "cn=xxx,dc=domain,dc=local"
LDAP_BIND_PASSWORD = "pass"
You can also use LDAP_BIND_PWD for the bind password. LDAP_BIND_PASSWORD
will be replaced with '****' in debug views like django_debug_toolbar
while LDAP_BIND_PWD
will not.
Optional parameter. If you want to change method of authorization with your LDAP from SIMPLE to any else (FIRST, SYNC, SIMPLE, NTLM), you can do it by setting parameter in settings.
from ldap3 import NTLM
LDAP_AUTHENTICATION = NTLM
Mandatory parameters. Search parameters enables you to define a first level of filter for the users and groups taken into account. Search base is used for every lookups so if you define it to the lower level, all users and groups of the directory are able to authenticate but if you define a subtree here, users and groups in other subtrees wont be able to do so. Search filters for users and groups enables you to get a second level of filtering by forcing some additional conditions to be true for the object to be taken into account. In LDAP, group membership is onwed by groups objects. Depending on the implementation (OpenLDAP, MS Active Directory, others), the attribute of group objects to inspect can differ. You need to determine the good attribute and name it in the LDAP_GROUP_MEMBER_ATTRIBUTE parameter for groups to be well used.
help for filters writing:
LDAP_SEARCH_BASE = "dc=domain,dc=local"
LDAP_USER_SEARCH_FILTER = "(&(sAMAccountName=%s)(objectClass=user))"
All %s
in LDAP_USER_SEARCH_FILTER
are replaced with {0}
and the username is injected with string.format
With this you can use the username more than once.
e.g.
LDAP_USER_SEARCH_FILTER = "(&(|(userPrincipalName={0})(sAMAccountName={0}))(objectClass=user))"
will match the username in either sAMAccountName
or userPrincipalName
so that users can then login with username or email address.
Mandatory parameter. As the module uses django's user class to create a "replicate" of the LDAP user to the database, it needs to know which LDAP attribute to map with which django User's attribute (it depends on the implementation and you onw use of it).
LDAP_ATTRIBUTES_MAP = {
'username': 'sAMAccountName',
'first_name': 'givenName',
'last_name': 'sn',
'email': 'mail',
}
Which user model field should be used as user login. Default: username
.
LDAP_USER_MODEL_USERNAME_FIELD = 'email'
Make sure field is mapped to LDAP attribute in LDAP_ATTRIBUTES_MAP
.
If this parameter is set, the module takes care that each connected user is member of this groups whereas you want to use LDAP groups or not.
LDAP_MIN_GROUPS = ["MyDjangoGroup", ]
If this parameter is set, the module will kept users in these locally-defined groups. Otherwise, every group membership is refreshed (removed and readded) when a user authenticates.
LDAP_IGNORED_LOCAL_GROUPS = ["MyLocalDjangoGroup", ]
First parameter of this group enables to use LDAP group binding or disable it to use local database groups only. A the first release of this module does have this parameter, for reverse compatibility, if this parameter does not exists, it is considered true. If it is configured to false, other parameters of this group wont be used nor checked. If LDAP_GROUPS_SEARCH_BASE is not defined LDAP_SEARCH_BASE will be used as base for group lookup. As django defines 2 special rights outside of the use of groups, the module can bind specific group membership to those 2 special attributes in addition to classical groups binding. If superuser and staff parameters are not present, not a list or an empty list, those parameters are skipped. This way, you can use LDAP groups for "classical groups" and define manually in the database who will be superuser or staff user. With the addition of LDAP engine, LDAP_GROUPS_SEARCH_FILTER can be automatically enriched in 2 different ways:
using LDAP_ENGINE = 'AD' or without specifying it: Thanks to jobec, LDAP_GROUPS_SEARCH_FILTER is now automatically enriched by the list of superuser, staff and map groups to limit search to the strictly needed ones and avoid troubles with LDAP having more than 1000 groups.
using LDAP_ENGINE = 'OpenLDAP': The filter is automatically enriched using the user's DN and getting only groups the user is member of and part of the superuser, staff and map groups list.
LDAP_USE_LDAP_GROUPS = True
LDAP_GROUPS_SEARCH_BASE = "dc=domain,dc=local"
LDAP_GROUPS_SEARCH_FILTER = "(&(objectClass=group))"
LDAP_GROUP_MEMBER_ATTRIBUTE = "member"
LDAP_SUPERUSER_GROUPS = ["CN=admin,dc=domain,dc=local", ]
LDAP_STAFF_GROUPS = ["CN=staff,dc=domain,dc=local", ]
LDAP_GROUPS_MAP = {
'my django group': "cn=my ldap group,dc=domain,dc=local",
}
You can ask this backend to store the LDAP user DN in the current session to be able to use it in another app.
LDAP_STORE_USER_DN = True
You will retrieve the value in the session this way:
request.session['LDAP_USER_DN']
You can ask this backend to determine and store the business unit of the user in the current session to be able to use it in another app. To do so, you need to set a dict in your settings to bind LDAP Organisational Unit to your own business unit codes
LDAP_STORE_BUSINESS_UNIT = {
'OU=myOU1,DC=mydom,DC=local' = 'myBU1',
'OU=myOU2,DC=mydom,DC=local' = 'myBU2',
}
You will retrieve your BU code in the session this way:
request.session['LDAP_USER_BU']
Mandatory parameter to tell django to use this module as it's authentication backend. See django's documentation to use it in an authentication chain. The example shows how to set it as the only authoritative authentication backend.
AUTHENTICATION_BACKENDS = ("django_auth_ldap3_ad.auth.LDAP3ADBackend",)
This auth module now comes with helpers to create a user in the Active Directory, update the password of the user, update the attributes of a user. Caution: those helpers, especially the create user one, are specific to MS Active Directory even if they use LDAP protocol.
As those helpers will make changes in the directory, you MUST configure almost one server to use SSL LDAP binding. Other servers in you configuration will be ignored automatically by those helpers. You must also provide a user with needed rights to add or modify users in the needed OU:
LDAP_BIND_ADMIN = "CN=my admin,OU=x,DC=mydom,DC=local"
LDAP_BIND_ADMIN_PASS = "MySuperFunPassword"
To be able to create the user principal name, you must also provide the local domain of the AD:
LDAP_AD_DOMAIN = "mydom.local"
The last setting is the path to the AD root certificate to enforce encryption between client and server:
LDAP_CERT_FILE = "/path/to/mycert.pem"
Once your configuration updated, you will be able to use the helpers:
from django_auth_ldap3_ad.ad_users import Aduser
adu = Aduser()
adu.create_ad_user(user_dn, firstname, lastname, sAMAccountName, mail=email, description=description)
adu.update_password_ad_user(user_dn, password)
adu.update_ad_user(user_dn, {'ldapAttrib1': 'value1', 'ldapAttrib2': 'value2'})
adu.activate_ad_user(user_dn)