Django Allauth Based Apis

Django is quite powerful framework. Amidst plenty of use-cases, one can even be tempted to choose Django as a REST API server, or a backend, as they usually call it. For sure, there are a dozen alternatives, some of which may be considered superior, like FastAPI or Flask, but today’s post is about Django.

People created many different packages for Django to make things simpler; the API scenario is not an exception. One of the most popular implementations is a prominent Django REST framework, or DRF for short.

When things come to authentication, another popular package appears, Django Allauth. Its authentication abilities are truly remarkable. Moreover, Allauth supports an integration with the DRF, giving you something like a Swiss-knife.

The first thing that came to my mind when I had started use these libraries was something like “did I really need the DRF at all?” Looking at the machinery behind Alluath, I could notice its similarity with the basic DRF. Thus, I tried to use Allauth as a single tool in my API projects.

I would stress that I do not think the DRF is completely useless. I just want to show a simple example where I replaced the DRF with Allauth. I am totally satisfied with the result, and maybe somebody find it compelling too. At the same time, I’m sure there are lots of cases where Allauth is not a suitable alternative, at least without a severe modification.

Starting a Project

Let’s create a new Django project. I use the uv tool in all my projects.

uv init proj
cd proj
uv add django-allauth

uv installs all necessary dependencies, including Django (6.0 in my case).

At the next step, create a Django project and application.

uv run django-admin startproject proj .
uv run manage.py startapp app

I don’t usually use a Django admin page with my API projects. So, get rid of the admin.py file inside the application sub-folder.

In all other projects, I delete models.py, views.py, and tests.py. It is better to create dedicated sub-folders and use standalone files for each model, view, or test later.

rm app/views.py
mkdir app/views
touch app/views/__init__.py
rm app/models.py
mkdir app/models
touch app/models/__init__.py
rm app/tests.py
mkdir app/tests
touch app/tests/__init__.py

So far, so good. It is time to edit the project settings file. Our application must be known to the project, so add it in INSTALLED_APPS list. Also, django.admin, django.messages, and django.staticfiles applications are considered useless in our case. Delete them.

The project is ready to adopt Allauth now. You can visit the headless section for more details.

# proj/settings.py
INSTALED_APPS = [
    'app',
    'allauth',
    'allauth.headless',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
]
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
]
MIDDLEWARE = [
    'django_prometheus.middleware.PrometheusBeforeMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'allauth.account.middleware.AccountMiddleware',
]
HEADLESS_ONLY = True
HEADLESS_FRONTEND_URLS = {}

The HEADLESS_FRONTEND_URLS dict is not important for us now, but you should pay attention to it in your headless projects.

The project urls.py file specifies routes for a headless dispatcher of Allauth, and for our application.

# proj/urls.py
from django.urls import include, path

urlpatterns = [
    path('', include('allauth.headless.urls')),
    path('', include('app.urls')),
]

Routing

As a network engineer, I find the naming of this feature in web applications especially funny. They call it routing, sir.

The project routes requests, which were not destined to the Allauth dispatcher, to our application, and we need to handle these requests. I took an approach Allauth uses for its url.py files, and added here a handy wrapper class, Route.

from allauth.headless.base.views import APIView
from allauth.headless.constants import Client
from django.urls import include, path

import app.views as v


class Route:
    def __init__(self, client: Client):
        self.client = client

    def __call__(self, route: str, view: type[APIView], name: str):
        return path(route, view.as_api_view(client=self.client), name=name)


def build_urlpatterns(client: Client):
    route = Route(client)

    patterns = [
        route('default', v.Default, 'get_default_view'),
    ]

    return [path('v1/', include(patterns))]


app_name = 'testing'

urlpatterns = [
    path('browser/', include((build_urlpatterns(Client.BROWSER), app_name), namespace='browser')),
    path('app/', include((build_urlpatterns(Client.APP), app_name), namespace='app')),
]

I believe it requires some explanation.

With the import app.views as v, we import all views from the app/views/ package. I prefer using a shortcut name v, to simplify a future extension.

Django expects the urlpatterns list in every urls.py file as an entry point. Just like in Allauth, we specify two main routes: browser and app. The former is for the web frontend, the latter is for mobile applications. Thus, all paths of our application start either with browser/ or with app/.

In both cases, the build_urlpatterns function completes the rest. It starts with the v1/ element introducing the versioning of our API. The patterns list contains all final paths. In our case, it contains a single default path that points to the v.Default view.

Thus, we have the following routes:

  • app/v1/default
  • browser/v1/default

Let’s add another one with the dynamic part within.

    patterns = [
        route('default', v.Default, 'get_default_view'),
        route('users/<str:id>', v.User, 'get_user'),
    ]

It is pretty easy to scale routes.

Views

All Allauth views are based on the APIView class, which is based on the RESTView class. That was a hint when I first saw it. No DRF, I decided.

For convenience, I made a custom class to wrap common responses Allauth uses internally, which is not mandatory. It also leaves room for a single point to modify all views.

from typing import Any

from allauth.headless.base.response import APIResponse
from allauth.headless.base.views import APIView
from django.core.handlers.wsgi import WSGIRequest


class BaseView(APIView):
    @staticmethod
    def response_200(request: WSGIRequest, data: Any = None, meta: Any = None):
        """Ok Response."""

        return APIResponse(request, data=data, meta=meta)

    @staticmethod
    def response_204(request: WSGIRequest):
        """No Content Response."""

        return APIResponse(request, status=204)

    @staticmethod
    def response_400(request: WSGIRequest, *, message: str, code='invalid', param='unknown'):
        """Bad Response."""

        error = {
            'message': message,
            'code': code,
            'param': param,
        }

        return APIResponse(request, errors=[error], status=400)

    @staticmethod
    def response_401(request: WSGIRequest):
        """Unauthen Response."""

        return APIResponse(request, status=401)

    @staticmethod
    def response_409(request: WSGIRequest):
        """Conflict Response."""

        return APIResponse(request, status=409)

Let’s put in to the app/views/base/ folder, and create the __init__.py file there.

from app.views.base.base import BaseView

__all__ = ('BaseView',)

It is time to describe our views.

Default View

Creating a view, we need not to forget to change the app/view/__init__.py file.

from app.views.default import Default

__all__ = ('Default',)

Our view returns a simple JSON object.

# app/views/default.py
from django.core.handlers.wsgi import WSGIRequest

from app.views.base import BaseView


class Default(BaseView):
    def get(self, request: WSGIRequest, *args, **kwargs):
        return self.response_200(request, {'foo': 'bar'})

Start the Django server in dev mode:

uv run manage.py runserver

And check our default path:

mc@bucket proj % curl 'http://localhost:8000/api/browser/v1/default'
{"status": 200, "data": {"foo": "bar"}}%
mc@bucket proj % 

User View

Again, do not forget to fill in the package index.

from app.views.default import Default
from app.views.users import User

__all__ = ('Default', 'User')

In this case, our view receives a dynamic value, which can be used, for example, during a database query later.

# app/views/users.py
from django.core.handlers.wsgi import WSGIRequest

from app.views.base import BaseView


class User(BaseView):
    def get(self, request: WSGIRequest, *args, **kwargs):
        uid = kwargs.get('id', 'default')
        return self.response_200(request, {'user_id': uid})

Authentication

Exposing any views as is, without any restrictions, is not always desirable. In many cases, APIs require authentication.

# app/views/base/utils.py
from functools import wraps
from typing import Any

from allauth.headless.base.response import AuthenticationResponse
from django.core.handlers.wsgi import WSGIRequest
from django.contrib.auth.models import AbstractUser


def requires_auth(func):
    @wraps(func)
    def wrapper(instance: Any, request: WSGIRequest, *args, **kwargs):
        if hasattr(request, 'user') and isinstance(request.user, AbstractUser) and request.user.is_authenticated:
            return func(*args, **kwargs)

        return AuthenticationResponse(request)

    return wrapper

This simple decorator verifies that a request has a user attached to it, and this user was authenticated before. In this case, it returns a response from a view it wraps.

The AuthenticationResponse is a response that Allauth uses during the current session status requests. The requires_auth uses it to return a 401 status response.

# app/views/users.py

from django.core.handlers.wsgi import WSGIRequest

from app.views.base import BaseView, requires_auth


class User(BaseView):
    @requires_auth
    def get(self, request: WSGIRequest, *args, **kwargs):
        uid = kwargs.get('id', 'default')
        return self.response_200(request, {'user_id': uid})

If we request it without the prior authentication:

mc@bucket proj % curl 'http://localhost:8000/browser/v1/users/8'
{"status": 401, "data": {"flows": [{"id": "login"}, {"id": "signup"}, {"id": "password_reset_by_code", "is_pending": false}]}, "meta": {"is_authenticated": false}}%    
mc@bucket proj % 

We are not authenticated yet.