How authentication works

django-modern-rest supports different auth workflows.

We support both:

  1. Checking that user requests contain required auth credentials

  2. 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:

  1. SyncAuth for sync controllers

  2. AsyncAuth for async controllers

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 but None, then we consider auth instance to succeed. If it returns None, we try the next one in the chain (if any). If it raises NotAuthenticatedError then 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:

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": []
          }
        ]
      }
    }
  }
}

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:

HTTP Basic

Support for HTTP’s default basic auth.

HTTP Basic Auth
Django Session

Support for Django’s default auth mechanism.

Django Session Auth
JWT Tokens

Support for JWT tokens based auth.

JWT 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 self if the login attempt was successful. Return None if login attempt failed and we need to try another authes. Raise dmr.exceptions.NotAuthenticatedError to immediately fail the login without trying other authes. Raise dmr.response.APIError if 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 self if the login attempt was successful. Return None if login attempt failed and we need to try another authes. Raise dmr.exceptions.NotAuthenticatedError to immediately fail the login without trying other authes. Raise dmr.response.APIError if 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.

class dmr.security.AuthenticatedHttpRequest[source]

Annotation for requests that used auth.

Use it for trusted controllers only.

Added in version 0.7.0.