diff --git a/docs/community/django-integration.md b/docs/community/django-integration.md new file mode 100644 index 000000000..0297c0746 --- /dev/null +++ b/docs/community/django-integration.md @@ -0,0 +1,212 @@ +--- +layout: default +title: Integrate Authelia with Django +parent: Community +nav_order: 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](https://docs.djangoproject.com/en/3.2/howto/auth-remote-user/) + +Therefore, it is possible to integrate Django with Authelia following the documentation about +[Proxy integration](https://www.authelia.com/docs/deployment/supported-proxies/#how-can-the-backend-be-aware-of-the-authenticated-users) +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 + +```python +# 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 + +```python +# 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 + +```python +# 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 +```python +# 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 + +```