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:
django-allauthowns the login flow and hands out session tokensdjango-modern-restturns those tokens into an authedself.request.useron 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:
1from allauth.headless.account.views import LoginView, SessionView
2from allauth.headless.constants import Client
3
4from dmr.openapi import build_schema
5from dmr.openapi.views import OpenAPIJsonView
6from dmr.routing import Router, external_path, path
7
8# `LOGIN_PATH_ITEM` and `SESSION_PATH_ITEM` describe what these two
9# `django-allauth` views answer, taken from its own specification:
10# https://docs.allauth.org/en/latest/headless/openapi-specification/
11from examples.auth.allauth.allauth_openapi import (
12 LOGIN_PATH_ITEM,
13 SESSION_PATH_ITEM,
14)
15
16router = Router(
17 'auth/',
18 urls=[
19 # `django-allauth` serves these two itself,
20 # `client=Client.APP` is what makes it hand out session tokens:
21 external_path(
22 'login/',
23 LoginView.as_api_view(client=Client.APP),
24 name='login',
25 openapi=LOGIN_PATH_ITEM,
26 ),
27 external_path(
28 'session/',
29 SessionView.as_api_view(client=Client.APP),
30 name='current_session',
31 openapi=SESSION_PATH_ITEM,
32 ),
33 ],
34)
35schema = build_schema(router)
36
37urlpatterns = [
38 router.to_urlpatterns(namespace='auth'),
39 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
40]
41
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:
XSessionTokenSyncAuthfor sync viewsXSessionTokenAsyncAuthfor async views
1from django.contrib.auth.models import User
2
3from dmr import Controller
4from dmr.plugins.pydantic import PydanticFastSerializer
5from dmr.security import AuthenticatedHttpRequest
6from dmr.security.allauth import XSessionTokenSyncAuth
7
8
9class APIController(Controller[PydanticFastSerializer]):
10 request: AuthenticatedHttpRequest[User]
11 auth = (XSessionTokenSyncAuth(),)
12
13 def get(self) -> str:
14 # Let's test that `User` has the correct type:
15 assert self.request.user.is_authenticated
16 return 'authed'
17
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_tokenHeader name, default:
X-Session-Tokenget_session_token()to read the token from somewhere else entirely, for example from theAuthorizationheader
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-allauthis an extra dependency, and a fairly large oneWe call
allauth.headless.internal.sessionkit, which lives inallauth’s internal namespace. This is the same entry point thatallauth’s own Django REST Framework and Django Ninja integrations use, so it is stable in practice, but it is not a public API contractSession tokens are Django sessions underneath, so they are stored in whatever
SESSION_ENGINEyou 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-Tokenheader. Override this if you want to extract the token from somewhere else, for example from theAuthorizationheader.
- 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.
- 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-allauthhas no async API, so the session lookup runs in a threadpool viaasgiref.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-Tokenheader. Override this if you want to extract the token from somewhere else, for example from theAuthorizationheader.
- 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.
- 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-allauthsession, if request was authed with it.This is the session that the token was resolved into, not
django.http.HttpRequest.sessionof the current request.When strict is passed and request has no such session, we raise
AttributeError.