Using Custom Authentication Backends in Django

Introduction

If you’re working in an organization with an established product line that serves live users, supporting a new site with Django probably means integrating with an existing authentication system. Many organizations use widely-adopted authentication systems provided by services like Google, Facebook, or GitHub. A few Python packages provide authentication integration with these services, but most of them expect you to be handling the final user accounts on with Django. What happens when you need to work with user accounts that live in another system altogether?

In this article, you’ll see the interface that Django exposes for authenticating to an external system. By the end, you should understand the pieces involved in mapping an external system’s information to Django’s native User objects in order to work with them on your own site.

Django’s default authentication

In the Django User Authentication System, we covered the basics of how default authentication works in Django. Ultimately, you can interact with User objects and understand if a user is_authenticated or not. Using the default authentication system, you can make use of many of Django’s built-in features like its login and logout views and password reset workflow.

When working with an external authentication system, you have to manage these pieces yourself. Some of them may not make sense to you depending on how your authentication system works.

Authentication Backends

As with many of Django’s systems, authentication is modeled as a plugin system. Django will try to authenticate users through a series of authentication backends. The default backend checks a user’s username and password against all the existing User objects in the database to authenticate them. The AUTHENTICATION_BACKENDS setting is your entrypoint to intercept this workflow and point Django to your external system.

An authentication backend is a class that, minimally, implements two methods:

  • get_user(user_id) — a user_id can be whatever unique identifier your external system uses to distinguish users, and get_user returns either a user object matching the given user_id or None.
  • authenticate(request, **credentials) — the request is the current HTTP request, and the credentials keyword arguments are whatever credentials your external system needs to check if a user should be authenticated or not. This is often a username and password, but it could be an API token or some other scheme. authenticate returns an authenticated User object or None.

Inside your authentication backend’s authenticate method, you can pass along the credentials to your external system via a REST API or another common authentication scheme like LDAP or SAML.

Using the wonderful Yes or No? API, you could build an authentication backend that authenticates a user occasionally if the API permits:

import requests

class FickleAuthBackend:
    def authenticate(self, request, username):
        response = requests.get(
            'https://yesno.wtf/api/'
        ).json()
        return User(username=username, password='') if response['answer'] == 'yes' else None

While authenticate can return a User object or None, it may also return an AnonymousUser object, or raise PermissionDenied to explicitly halt any further authentication checks. This allows for a variety of ways to proceed, and anonymous users may still have certain permissions. You’ll want to account for that in your middleware and views.

If the external user service provides additional information about the user, get_user might be a good place to grab some of that data. You can add attributes to the user object in authenticate before you return it if you’d like, but be careful of how many attributes you add dynamically.

Permissions

I also covered Django’s permission scheme in The Django User Authentication System: when given a user, you can inquire about their permissions generally or against specific objects using the has_perm method. Custom authentication backends can override permission checking methods and Django will check against those first before falling back to its default checks. This allows you to make queries to your external system about permissions in addition to authentication:

...
def has_perm(self, user_obj, perm, obj=None):
    response = requests.get(
        'https://yesno.wtf/api/'
    ).json()
    return response['answer'] == 'yes'

has_perm can also raise PermissionDenied to halt further authorization checks, similar to authenticate.

Extending and customizing user models

If you’d like to fully integrate Django with your external system, there’s much more you can do by way of the User model. I won’t dive too deeply into that portion of Django, but it is fully laid out in Customizing authentication in Django.

This kind of customization lets you use the built-in behaviors of a user while adding your own information and behaviors through proxy models, or one-to-one mappings to custom models. For example, you can pull in information from your external system, creating a new user in your Django database each time a new user authenticates for the first time.

If you’re working in an ecosystem with a mature external user management service, I recommend consistently keeping user-related data and behavior there instead of fragmenting it into your Django code.

For internal tools or tools with a separate audience and differing information storage needs, though, custom user models may work well for you.

Conclusion

Django provides a flexible and extensible way to customize user authentication, whether you want to let another system do most of the user account management or want to do it yourself. Using custom authentication backends, you can easily integrate with external systems using almost anything you do in Python. These integrations give you the power to customize permissions checking as well, opening the floor for many possibilities all while working within Django’s native interfaces.