authelia/docs/community/django-integration.md
Biel Frontera 8fc48476c6
docs: add community example of integrating authelia with Django (#2858)
Extend the information given at #2636 about how to integrate
authelia with Django.
2022-02-09 21:50:34 +01:00

7.3 KiB

layout title parent nav_order
default Integrate Authelia with Django Community 6

Integrate Authelia with Django

Django, the Python web framework, can be configured to delegate authentication to external services using HTTP request headers. This is well documented on Django documentation

Therefore, it is possible to integrate Django with Authelia following the documentation about Proxy integration and adding a few lines of code on your Django application.

Basic integration

Django uses REMOTE_USER header by default. But WSGI servers transform the headers received from proxy servers adding HTTP_ as prefix. So we need to add a custom middleware in order to use HTTP_REMOTE_USER.

This basic configuration enables authentication using Authelia. If the user does not exists on Django database, it will be automatically created.

Configuration

# file: settings.py

MIDDLEWARE = [
    '...',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'your_app.auth.middleware.RemoteUserMiddleware',
    # or 'your_app.auth.middleware.PersistentRemoteUserMiddleware',
    '...',
]

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.RemoteUserBackend',
]

# Logout from authelia after logout on the Django application
LOGOUT_REDIRECT_URL = 'https://auth.your_domain.com/logout'

New authentication middleware

# new file: your_app/auth/middleware.py
from django.contrib.auth.middleware import RemoteUserMiddleware, PersistentRemoteUserMiddleware


class HttpRemoteUserMiddleware(RemoteUserMiddleware):
    header = 'HTTP_REMOTE_USER'

    # uncomment the line below to disable authentication to users that not exists on Django database
    # create_unknown_user = False 


class PersistentHttpRemoteUserMiddleware(PersistentRemoteUserMiddleware):
    """
    The RemoteUserMiddleware authentication middleware assumes that the HTTP request header 
    REMOTE_USER is present with all authenticated requests.

    With PersistentRemoteUserMiddleware, it is possible to receive this header only on a few 
    pages (as login page) and maintain the authenticated session until explicit 
    logout by the user.
    """
    header = 'HTTP_REMOTE_USER' 

Security Warning: The proxy server must set Remote-User header every time it hits the Django application. If you only protect the login URL with Authelia and use the Persistent class, you have to set this header to '' on the other locations.

Advanced integration

While the basic integration only uses the HTTP header Remote-User set by Authelia, this advanced integration uses also the HTTP headers Remote-Name, Remote-Email and Remote-Groups.

In this example, we create a new authentication backend on Django that will synchronize user data with Authelia backend, storing the name, the email and the groups of the user on the Django database.

Configuration

# file: settings.py

MIDDLEWARE = [
    '...',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'your_app.auth.middleware.RemoteUserMiddleware',
    # or 'your_app.auth.middleware.PersistentRemoteUserMiddleware',
    '...',
]

AUTHENTICATION_BACKENDS = [
    'your_app.auth.backends.RemoteExtendedUserBackend',
]

# Logout from authelia after logout on the Django application
LOGOUT_REDIRECT_URL = 'https://auth.your_domain.com/logout'

New authentication backend

# new file: your_app/auth/backends.py
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.backends import RemoteUserBackend


class RemoteExtendedUserBackend(RemoteUserBackend):
    """
    This backend can be used in conjunction with the ``RemoteUserMiddleware``
    to handle authentication outside Django and update local user with external information
    (name, email and groups).

    Extends RemoteUserBackend (it creates the Django user if it does not exist,
    as explained here: https://github.com/django/django/blob/main/django/contrib/auth/backends.py#L167),
    updating the user with the information received from the remote headers.

    Django user is only added to groups that already exist on the database (no groups are created).
    A settings variable can be used to exclude some groups when updating the user.
    """

    excluded_groups = set()
    if hasattr(settings, 'REMOTE_AUTH_BACKEND_EXCLUDED_GROUPS'):
        excluded_groups = set(settings.REMOTE_AUTH_BACKEND_EXCLUDED_GROUPS)

    # Warning: possible security breach if reverse proxy does not set
    # these variables EVERY TIME it hits this Django application (and REMOTE_USER variable).
    # See https://docs.djangoproject.com/en/4.0/howto/auth-remote-user/#configuration
    header_name = 'HTTP_REMOTE_NAME'
    header_groups = 'HTTP_REMOTE_GROUPS'
    header_email = 'HTTP_REMOTE_EMAIL'

    def authenticate(self, request, remote_user):
        user = super().authenticate(request, remote_user)

        # original authenticate calls configure_user only
        # when user is created. We need to call this method every time
        # the user is authenticated in order to update its data.
        if user:
            self.configure_user(request, user)
        return user

    def configure_user(self, request, user):
        """
        Complete the user from extra request.META information.
        """
        if self.header_name in request.META:
            user.last_name = request.META[self.header_name]

        if self.header_email in request.META:
            user.email = request.META[self.header_email]

        if self.header_groups in request.META:
            self.update_groups(user, request.META[self.header_groups])

        if self.user_has_to_be_staff(user):
            user.is_staff = True

        user.save()
        return user

    def user_has_to_be_staff(self, user):
        return True

    def update_groups(self, user, remote_groups):
        """
        Synchronizes groups the user belongs to with remote information.

        Groups (existing django groups or remote groups) on excluded_groups are completely ignored.
        No group will be created on the django database.

        Disclaimer: this method is strongly inspired by the LDAPBackend from django-auth-ldap.
        """
        current_group_names = frozenset(
            user.groups.values_list("name", flat=True).iterator()
        )
        preserved_group_names = current_group_names.intersection(self.excluded_groups)
        current_group_names = current_group_names - self.excluded_groups

        target_group_names = frozenset(
            [x for x in map(self.clean_groupname, remote_groups.split(',')) if x is not None]
        )
        target_group_names = target_group_names - self.excluded_groups

        if target_group_names != current_group_names:
            target_group_names = target_group_names.union(preserved_group_names)
            existing_groups = list(
                Group.objects.filter(name__in=target_group_names).iterator()
            )
            user.groups.set(existing_groups)
        return

    def clean_groupname(self, groupname):
        """
        Perform any cleaning on the "groupname" prior to using it.
        Return the cleaned groupname.
        """
        return groupname