docs: add community example of integrating authelia with Django (#2858)
Extend the information given at #2636 about how to integrate authelia with Django.pull/2855/head^2
parent
9c98321130
commit
8fc48476c6
|
@ -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
|
||||||
|
|
||||||
|
```
|
Loading…
Reference in New Issue