How authentication works¶
django-modern-rest supports different auth workflows.
We support both:
Checking that user requests contain required auth credentials
Boilerplate code for views that provide credentials for users
Enabling auth¶
Let’s start with how auth can be enabled and how it works.
There are two main base classes for auth:
Warning
Sync controllers can’t directly use async auth. And async controllers can’t directly use sync auth.
All auth - that we are going to use - will be instances of these two classes (and their subclasses).
All of them have unified API:
__init__method contains configuration that can be changed per instance__call__()does all the heavy lifting. If__call__returns anything butNone, then we consider auth instance to succeed. If it returnsNone, we try the next one in the chain (if any). If it raisesNotAuthenticatedErrorthen we immediately stop and return the error response. Async auth has async__call__, sync auth has sync one.security_schemes()provides OpenAPI spec to define this auth method in the spec.security_requirement()provides OpenAPI spec to indicate what kind of auth will be required for each endpoint using this auth.
Some classes provide configuration to be adjusted when creating instances.
For example: JWTSyncAuth
contains multiple options in its __init__ method.
There are 4 ways to provide auth classes for an endpoint:
1from dmr import Controller, modify
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.security.django_session import DjangoSessionSyncAuth
4
5
6class APIController(Controller[PydanticSerializer]):
7 @modify(auth=[DjangoSessionSyncAuth()])
8 def get(self) -> str:
9 return 'authed'
10
Run result
$ curl http://127.0.0.1:8000/api/example/ -D - -X GET
HTTP/1.1 401 Unauthorized
date: Fri, 05 Jun 2026 12:21:36 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language, Cookie
Content-Language: en
Content-Length: 58
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Not authenticated","type":"security"}]}
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": {
"csrf": {
"description": "CSRF protection",
"in": "cookie",
"name": "csrftoken",
"type": "apiKey"
},
"django_session": {
"description": "Reusing standard Django auth flow for API",
"in": "cookie",
"name": "sessionid",
"type": "apiKey"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/apicontroller/": {
"get": {
"deprecated": false,
"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"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when CSRF check failed"
},
"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": [
{
"csrf": [],
"django_session": []
}
]
}
}
}
}
1from dmr import Controller
2from dmr.plugins.pydantic import PydanticSerializer
3from dmr.security.jwt import JWTAsyncAuth
4
5
6class APIController(Controller[PydanticSerializer]):
7 auth = (JWTAsyncAuth(),)
8
9 async def get(self) -> str:
10 return 'authed'
11
Run result
$ curl http://127.0.0.1:8000/api/example/ -D - -X GET
HTTP/1.1 401 Unauthorized
date: Fri, 05 Jun 2026 12:21:37 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 58
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Not authenticated","type":"security"}]}
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": {
"jwt": {
"bearerFormat": "JWT",
"description": "JWT token auth",
"scheme": "Bearer",
"type": "http"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/apicontroller/": {
"get": {
"deprecated": false,
"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": [
{
"jwt": []
}
]
}
}
}
}
1>>> from dmr.settings import Settings, DMR_SETTINGS
2>>> from dmr.security.django_session import DjangoSessionSyncAuth
3
4>>> DMR_SETTINGS = {Settings.auth: [DjangoSessionSyncAuth()]}
Providing several auth instances means that at least one of them must succeed.
Disabling auth¶
It is a common practice to define a global auth protocol
in settings and then disable auth per specific endpoints
like /registration and /login.
To do so, set auth=None for the specific
endpoints / controllers that should not have auth.
Setting None as auth in any place will always disable
all auth in further layers.
Note
We don’t allow setting Settings.auth to None,
because it will globally disable all auth with no ways to re-enable it.
Permissions¶
Many similar frameworks also include different abstractions
for defining permissions classes, like:
guards=[UserHasPermissions('delete')] or IsSuperUser(), etc.
We don’t do that on purpose. This is not a framework logic, this is your business logic. It should be placed inside your code, not ours.
Making proper abstractions inside your own code base will allow you to:
Make it super specific for your usecase
Make it optimized
Make it clean and consistent with other business rules you will have
Yes, these permissions can look cool in a framework on paper, but they do not serve a good purpose in large codebases in reality.
Focus on your domain, not on framework.
Next up¶
Select auth backend that fits your needs:
Support for HTTP’s default basic auth.
Support for Django’s default auth mechanism.
Support for JWT tokens based auth.
API Reference¶
- class dmr.security.SyncAuth[source]¶
Sync auth base class for sync endpoints.
All auth must support initialization without any required parameters. Auth can have non-required parameters with defaults.
- abstractmethod __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) Self | None[source]¶
Put your auth business logic here.
Return
selfif the login attempt was successful. ReturnNoneif login attempt failed and we need to try another authes. Raisedmr.exceptions.NotAuthenticatedErrorto immediately fail the login without trying other authes. Raisedmr.response.APIErrorif you want to change the return code, for example, when some data is missing or has wrong format.
- 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.
- abstract property security_requirement: dict[str, list[str]]¶
Provides a security schema usage requirement.
- abstract property security_schemes: dict[str, SecurityScheme | Reference]¶
Provides a security schema definition.
- class dmr.security.AsyncAuth[source]¶
Async auth base class for async endpoints.
All auth must support initialization without any required parameters. Auth can have non-required parameters with defaults.
- abstractmethod async __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) Self | None[source]¶
Put your auth business logic here.
Return
selfif the login attempt was successful. ReturnNoneif login attempt failed and we need to try another authes. Raisedmr.exceptions.NotAuthenticatedErrorto immediately fail the login without trying other authes. Raisedmr.response.APIErrorif you want to change the return code, for example, when some data is missing or has wrong format.
- 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.
- abstract property security_requirement: dict[str, list[str]]¶
Provides a security schema usage requirement.
- abstract property security_schemes: dict[str, SecurityScheme | Reference]¶
Provides a security schema definition.
- dmr.security.request_auth(request: HttpRequest, *, strict: Literal[True]) SyncAuth | AsyncAuth[source]¶
- dmr.security.request_auth(request: HttpRequest, *, strict: bool = False) SyncAuth | AsyncAuth | None
Return the auth instance that was used to auth this request.
When strict is passed and request has no auth, we raise
AttributeError.