Combine LDAP and classical authentication in django
Still in our CAMM project JOB at Makina Corpus, we needed to provide an authentication system through a LDAP server. The module django-auth-ldap does most of work itself but we decided to combine this LDAP authentication with the classical django authentication for two reasons:
- keep possibility to create users only in django without having them in the LDAP directory (mainly for superusers);
- be able to fallback on django authentication for all users in case of a LDAP server failure.
So to handle this, we have to:
- differentiate LDAP directory users from django database users;
- store password of LDAP users in django to be able to switch on classical authentication;
- implement two custom authentication backends.
User model
We just implement a custom model user with a boolean from_ldap to diffentiate LDAP users from classical django users.
class MyUser(AbstractUser):
from_ldap = models.BooleanField(
_('LDAP user'),
editable=False,
default=False)
LDAP authentication backend
This LDAP backend has two goals :
- Save user password in django database, thus he'll be able to log in django if LDAP authentication backend is disabled;
- Force from_ldap field to True when a user is created by this way.
from django_auth_ldap.backend import LDAPBackend
from django.contrib.auth import get_user_model
class MyLDAPBackend(LDAPBackend):
""" A custom LDAP authentication backend """
def authenticate(self, username, password):
""" Overrides LDAPBackend.authenticate to save user password in django """
user = LDAPBackend.authenticate(self, username, password)
# If user has successfully logged, save his password in django database
if user:
user.set_password(password)
user.save()
return user
def get_or_create_user(self, username, ldap_user):
""" Overrides LDAPBackend.get_or_create_user to force from_ldap to True """
kwargs = {
'username': username,
'defaults': {'from_ldap': True}
}
user_model = get_user_model()
return user_model.objects.get_or_create(**kwargs)
Classical authentication backend
We override django.contrib.auth.backends.ModelBackend to ensure LDAP users can't logged in with this one if MyLDAPBackend is available.
from django.contrib.auth import get_backends, get_user_model
from django.contrib.auth.backends import ModelBackend
class MyAuthBackend(ModelBackend):
""" A custom authentication backend overriding django ModelBackend """
@staticmethod
def _is_ldap_backend_activated():
""" Returns True if MyLDAPBackend is activated """
return MyLDAPBackend in [b.__class__ for b in get_backends()]
def authenticate(self, username, password):
""" Overrides ModelBackend to refuse LDAP users if MyLDAPBackend is activated """
if self._is_ldap_backend_activated():
user_model = get_user_model()
try:
user_model.objects.get(username=username, from_ldap=False)
except:
return None
user = ModelBackend.authenticate(self, username, password)
return user
Django settings and fallback solution
Normally, we have our two backends activated :
- LDAP users can only connect through MyLDAPBackend;
- Django users can connect through MyAuthBackend.
AUTHENTICATION_BACKENDS = (
'accounts.backends.MyLDAPBackend',
'accounts.backends.MyAuthBackend',
)
In case of a LDAP directory failure, we just have to disable MyLDAPBackend and everybody can connect with MyAuthBackend.
AUTHENTICATION_BACKENDS = (
#'accounts.backends.MyLDAPBackend',
'accounts.backends.MyAuthBackend',
)
Thanks leplatrem for review!
[FR] Ce billet en français sur le blog de Makina Corpus !
Comments !