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 !