Skip to main content

Authentication Backends

The authentication system in this codebase is built on a pluggable backend architecture. This design decouples the logic of verifying credentials from the user model itself, allowing the system to authenticate users against multiple sources—such as a local database, an LDAP server, or a remote OAuth provider—simultaneously.

The behavior is controlled by the AUTHENTICATION_BACKENDS setting, which defines an ordered list of backend classes. When a user attempts to log in, the system iterates through these backends until one successfully returns a user object.

The Backend Interface

All authentication backends should inherit from django.contrib.auth.backends.BaseBackend. This class defines the standard interface but provides "fail-safe" default implementations that return None for authentication and empty sets for permissions.

class BaseBackend:
def authenticate(self, request, **kwargs):
return None

def get_user(self, user_id):
return None

def get_all_permissions(self, user_obj, obj=None):
return {
*self.get_user_permissions(user_obj, obj=obj),
*self.get_group_permissions(user_obj, obj=obj),
}

By returning None by default, BaseBackend ensures that a custom backend does not accidentally grant access or permissions unless specifically implemented. The interface also includes asynchronous counterparts like aauthenticate and aget_user, which wrap the synchronous methods using sync_to_async to support modern async workflows.

Database Authentication

The ModelBackend is the default implementation and the primary way users are authenticated against the local database. It retrieves users based on the USERNAME_FIELD of the configured AUTH_USER_MODEL.

Security and Timing Attacks

A critical design choice in ModelBackend.authenticate is the use of check_password_with_timing_attack_mitigation. This ensures that the time taken to process a login request is consistent, whether the username exists or not, preventing attackers from enumerating valid usernames based on response times.

def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
# ... lookup user ...
if check_password_with_timing_attack_mitigation(
user, password
) and self.user_can_authenticate(user):
return user

The Permission System

ModelBackend also serves as the engine for Django's permission system. It calculates permissions from three sources:

  1. User Permissions: Permissions assigned directly to the user.
  2. Group Permissions: Permissions inherited from groups the user belongs to.
  3. Superuser Status: If is_superuser is True, the backend bypasses checks and grants all permissions.

To optimize performance, ModelBackend caches these permissions on the user object (e.g., in _perm_cache or _user_perm_cache). This prevents redundant database queries when has_perm() is called multiple times during a single request-response cycle.

External and Remote Authentication

For environments where authentication is handled by a web server (like Apache or Nginx) or an external SSO provider, the codebase provides RemoteUserBackend and its associated middleware.

The Chain of Trust

This system relies on a chain of trust:

  1. Web Server: Authenticates the user and sets a header (defaulting to REMOTE_USER).
  2. RemoteUserMiddleware: Detects this header and passes the username to the backend.
  3. RemoteUserBackend: Validates the username and retrieves or creates a local User record.

The RemoteUserBackend includes a create_unknown_user attribute (defaulting to True). When enabled, it automatically provisions a local user account the first time an external user is recognized, allowing the application to manage local profile data and permissions for externally authenticated users.

Persistent vs. Strict Sessions

The RemoteUserMiddleware is strict by default: if the REMOTE_USER header disappears, it logs the user out. This is ideal for high-security environments. However, for setups where external auth is only required for the initial login, PersistentRemoteUserMiddleware can be used. It sets force_logout_if_no_header = False, allowing the Django session to persist even after the header is removed.

Customizing Authentication

Developers can extend the authentication system by subclassing BaseBackend or ModelBackend. Common reasons for customization include:

  • Cleaning Usernames: Overriding clean_username in RemoteUserBackend to strip domain names (e.g., converting user@domain.com to user).
  • Custom User Configuration: Overriding configure_user to set default attributes or sync profile data from external headers during the first login.
  • Bypassing Activity Checks: By default, ModelBackend rejects users where is_active is False. The AllowAllUsersModelBackend variant overrides user_can_authenticate to allow these users to log in, which is useful for specific administrative or debugging scenarios.
class AllowAllUsersModelBackend(ModelBackend):
def user_can_authenticate(self, user):
return True

Tradeoffs and Constraints

While the pluggable architecture is flexible, it introduces certain constraints:

  • Middleware Ordering: RemoteUserMiddleware must be placed after AuthenticationMiddleware in the MIDDLEWARE setting because it relies on request.user being initialized.
  • Permission Caching: Because permissions are cached on the user object, changes made to a user's permissions during a request may not be reflected until the next request, unless the cache is manually cleared.
  • Credential Requirements: ModelBackend.authenticate strictly requires both a username and a password. If a custom authentication flow only provides one (e.g., token-based auth), a custom backend must be implemented to handle those specific keyword arguments.