django-allauth#

Docs: https://docs.allauth.org/en/latest/headless/index.html

Important

To use allauth features you must install django-allauth separately.

django-allauth is a well-established, full-featured library that already solves the hard parts of auth: registration, email verification, password reset, social login, MFA, and passkeys. Its headless mode exposes all of that over a plain JSON API that is not coupled to any REST framework.

So we do not reimplement any of it. Instead:

  1. django-allauth owns the login flow and hands out session tokens

  2. django-modern-rest turns those tokens into an authed self.request.user on your own endpoints

Note

Add 'allauth', 'allauth.account', and 'allauth.headless' to INSTALLED_APPS, and 'allauth.account.middleware.AccountMiddleware' to MIDDLEWARE.

See the allauth installation docs.

How it works#

In headless mode django-allauth supports two client types. Session tokens are used by the app client, which is the one you want for mobile apps and other non-browser API consumers.

        ---
config:
  theme: forest

---
sequenceDiagram
    participant C as Client
    participant A as django-allauth
    participant D as Your dmr API

    C->>A: POST /_allauth/app/v1/auth/login
    A-->>C: X-Session-Token
    C->>D: GET /api/me/ (X-Session-Token)
    D->>D: XSessionTokenSyncAuth resolves the token
    D-->>C: 200, authed as request.user
    

Session token flow#

The login half is served by django-allauth itself.

Routing the headless views#

django-allauth ships the login and session endpoints as ordinary Django views, so you can mount them into your own Router with external_path() and describe them in your own OpenAPI schema. See external views for the details.

LoginView hands out the session token, SessionView inspects it on GET and logs out on DELETE. Both need to know which headless client they serve, and session tokens only exist for the app one:

OpenAPI Schema

Preview openapi.json
{
  "components": {},
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/auth/login/": {
      "post": {
        "operationId": "login",
        "responses": {
          "200": {
            "description": "The session token is in the body"
          },
          "400": {
            "description": "Missing fields or wrong credentials"
          }
        },
        "summary": "Log in and receive a session token"
      }
    },
    "/auth/session/": {
      "delete": {
        "operationId": "logout",
        "responses": {
          "401": {
            "description": "Logged out, the token no longer resolves"
          }
        },
        "summary": "Log out and invalidate the session token"
      },
      "get": {
        "operationId": "currentSession",
        "responses": {
          "200": {
            "description": "The session is valid"
          },
          "401": {
            "description": "No or unknown session token"
          }
        },
        "summary": "Inspect the session behind the token"
      }
    }
  }
}

Your own endpoints then sit behind XSessionTokenSyncAuth, as shown below.

Warning

Describing a third-party view in your schema makes it part of your API contract, including for schema-driven tests. django-allauth’s views do not always fit a strict contract: the login view answers 400 instead of 405 for unsupported methods, and returns 500 for a JSON body that is not an object. Pass openapi=None to route such a view without publishing it.

Requiring auth#

Note

Current user will always be accessible as self.request.user.

Read more: https://docs.djangoproject.com/en/stable/topics/auth/default/

We provide two classes to require an allauth session token:

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      }
    },
    "securitySchemes": {
      "session_token": {
        "description": "`django-allauth` headless session token",
        "in": "header",
        "name": "X-Session-Token",
        "type": "apiKey"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/apicontroller/": {
      "get": {
        "operationId": "getAPIControllerApiApicontroller",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "OK"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when auth was not successful"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        },
        "security": [
          {
            "session_token": []
          }
        ]
      }
    }
  }
}

Custom user models are automatically supported.

You can customize:

  • Security scheme name, default: session_token

  • Header name, default: X-Session-Token

  • get_session_token() to read the token from somewhere else entirely, for example from the Authorization header

Note

Tokens are read from a header, never from a cookie. Browsers do not attach headers automatically on cross-site requests, so unlike Django Session Auth there is no CSRF check here.

Accessing the allauth session#

django-allauth resolves a token into a Django session object. Use request_allauth_session() if you need to read anything from it.

Warning

This is the session the token resolved into, not django.http.HttpRequest.session of the current request. We do not log the user into the current session, because that would issue a session cookie for a cookie-less API.

Async support#

django-allauth has no async API at the moment, so XSessionTokenAsyncAuth runs the session lookup in a threadpool via asgiref.sync.sync_to_async.

It works correctly, but it does not give you the full benefit of async I/O. If that matters for your workload, prefer Token Auth or JWT Auth, which use native async ORM calls.

Trade-offs#

Things worth knowing before choosing this auth:

  • django-allauth is an extra dependency, and a fairly large one

  • We call allauth.headless.internal.sessionkit, which lives in allauth’s internal namespace. This is the same entry point that allauth’s own Django REST Framework and Django Ninja integrations use, so it is stable in practice, but it is not a public API contract

  • Session tokens are Django sessions underneath, so they are stored in whatever SESSION_ENGINE you configured, and hitting the API costs a session store lookup per request

If you only need plain token auth without allauth’s account features, Token Auth is simpler and has no third-party dependency.

API Reference#

class dmr.security.allauth.auth.XSessionTokenSyncAuth(*, header_name: str = 'X-Session-Token', security_scheme_name: str = 'session_token')[source]#

Sync auth for django-allauth’s headless session tokens.

Added in version 0.15.0.

__call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) → Self | None[source]#

Does check for the correct session token.

authenticate(request: HttpRequest, token: str) → AbstractBaseUser[source]#

Run all auth pipeline.

get_session_token(request: HttpRequest) → str | None#

Return the raw session token for the given request.

By default we look up the X-Session-Token header. Override this if you want to extract the token from somewhere else, for example from the Authorization header.

provide_response_specs(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]], existing_responses: Mapping[HTTPStatus, ResponseSpec]) → list[ResponseSpec]#

Provides responses that can happen when user is not authed.

security_requirements(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]]) → list[dict[str, list[str]]]#

Provides a security schema usage requirement.

security_schemes(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]]) → dict[str, SecurityScheme | Reference]#

Provides a security schema definition.

set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, session: SessionBase) → None[source]#

Set current user as authed for this request.

validate(controller_cls: type[Controller[BaseSerializer]], metadata: EndpointMetadata) → None#

Validate auth configuration at import time.

Added in version 0.16.0.

property www_authenticate_challenge: str | None#

Session tokens have no challenge, so this returns None.

A challenge names a scheme for the Authorization header, while X-Session-Token is not an authentication scheme.

class dmr.security.allauth.auth.XSessionTokenAsyncAuth(*, header_name: str = 'X-Session-Token', security_scheme_name: str = 'session_token')[source]#

Async auth for django-allauth’s headless session tokens.

django-allauth has no async API, so the session lookup runs in a threadpool via asgiref.sync.sync_to_async.

Added in version 0.15.0.

async __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) → Self | None[source]#

Does check for the correct session token.

async authenticate(request: HttpRequest, token: str) → AbstractBaseUser[source]#

Run all auth pipeline.

get_session_token(request: HttpRequest) → str | None#

Return the raw session token for the given request.

By default we look up the X-Session-Token header. Override this if you want to extract the token from somewhere else, for example from the Authorization header.

provide_response_specs(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]], existing_responses: Mapping[HTTPStatus, ResponseSpec]) → list[ResponseSpec]#

Provides responses that can happen when user is not authed.

security_requirements(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]]) → list[dict[str, list[str]]]#

Provides a security schema usage requirement.

security_schemes(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]]) → dict[str, SecurityScheme | Reference]#

Provides a security schema definition.

async set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, session: SessionBase) → None[source]#

Set current user as authed for this request.

validate(controller_cls: type[Controller[BaseSerializer]], metadata: EndpointMetadata) → None#

Validate auth configuration at import time.

Added in version 0.16.0.

property www_authenticate_challenge: str | None#

Session tokens have no challenge, so this returns None.

A challenge names a scheme for the Authorization header, while X-Session-Token is not an authentication scheme.

dmr.security.allauth.auth.request_allauth_session(request: HttpRequest, *, strict: Literal[True]) → SessionBase[source]#
dmr.security.allauth.auth.request_allauth_session(request: HttpRequest, *, strict: bool = False) → SessionBase | None

Returns the django-allauth session, if request was authed with it.

This is the session that the token was resolved into, not django.http.HttpRequest.session of the current request.

When strict is passed and request has no such session, we raise AttributeError.