Token Auth#

Opaque token authentication backed by database hashed records.

Note

To use token auth with the default Token model, add 'dmr.security.token.app' to INSTALLED_APPS and run migrations with manage.py migrate.

You can subclass all classes, change token_model property and substitute the token model for your own one. More on that later.

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 several classes to require token auth in your API for both sync and async endpoints.

Example of requiring token auth and accessing both self.request.user and the current token:

When in doubt, use this as the default way to receive tokens.

Use HeaderTokenSyncAuth and HeaderTokenAsyncAuth for header-based auth.

You can customize:

  • Security scheme name, default: token

  • Header name, default: X-API-Token

  • Header value prefix, default: ''

  • Advanced token parameters

Run result

$ curl http://127.0.0.1:8000/api/users/ -X GET -H 'X-API-Token: XMylfxW1WwVB5AlUXpZkgl9Ir4BHHr-M1DI7i1Ue_5M'
"authed"

$ curl http://127.0.0.1:8000/api/users/ -D - -X GET -H 'X-API-Token: wrong-token'
HTTP/1.1 401 Unauthorized
date: Mon, 05 Oct 2026 11:16:30 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": {
      "token": {
        "description": "Opaque token authentication",
        "in": "header",
        "name": "X-API-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": [
          {
            "token": []
          }
        ]
      }
    }
  }
}

Customizing auth#

Token Auth supports a lot of customization options: starting from token_size up to the secret key customization.

See issue() for more info on all configuration options.

Token lifecycle#

Important

Token text itself can only be obtained once. This is an important security limitation by design. Why? Because we never store the token text itself, we only store its hash.

        ---
config:
  theme: forest

---
stateDiagram-v2
    [*] --> Active: TokenLike.issue / TokenLike.aissue
    Active --> Revoked: TokenLike.revoke / TokenLike.arevoke
    Active --> Expired
    

Default token model states#

All token instances are issued and revoked in sync mode via dmr.security.token.token.TokenLikeSync and in async mode via dmr.security.token.token.TokenLikeAsync interfaces.

Our default implementation dmr.security.token.app.models.Token strictly follows both interfaces.

Issuing a token#

To issue a token one can use two main strategies:

  1. Manually call:

    methods and copy the token. This might be useful if you only want to have just a couple of clients.

  2. Provide an API view for users to obtain tokens when they need them.

Ready-to-use obtain views#

dmr.security.token.concrete_views has controllers that issue tokens and only need a serializer, the token model is optional:

Pass the serializer to as_view in your urls, there is no view code at all:

Run result

$ curl http://127.0.0.1:8000/api/auth/ -X POST -d '{"username": "test_user", "password": "password"}' -H 'Content-Type: application/json'
{"token":"UbdtUtOQVrjwUdyz2BRxQFovxy3Y8ZgsKHatZiW0ThA"}

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"
      },
      "ObtainTokenPayload": {
        "description": "Payload for default version of an opaque token request body.\n\nIs also used as kwargs for :func:`django.contrib.auth.authenticate`.",
        "properties": {
          "password": {
            "title": "Password",
            "type": "string"
          },
          "username": {
            "title": "Username",
            "type": "string"
          }
        },
        "required": [
          "username",
          "password"
        ],
        "title": "ObtainTokenPayload",
        "type": "object"
      },
      "ObtainTokenResponse": {
        "description": "Default response type for opaque token endpoint.",
        "properties": {
          "token": {
            "title": "Token",
            "type": "string"
          }
        },
        "required": [
          "token"
        ],
        "title": "ObtainTokenResponse",
        "type": "object"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/auth/": {
      "post": {
        "operationId": "postObtainTokenSyncControllerApiAuth",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ObtainTokenPayload"
              }
            }
          },
          "description": "Payload for default version of an opaque token request body.\n\nIs also used as kwargs for :func:`django.contrib.auth.authenticate`.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ObtainTokenResponse"
                }
              }
            },
            "description": "OK",
            "headers": {
              "Cache-Control": {
                "description": "Credentials must not be stored in any cache.",
                "required": true,
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Unauthorized"
          },
          "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"
          }
        },
        "summary": "By default tokens are acquired on post."
      }
    }
  }
}

They take a username and a password as ObtainTokenPayload and return a new token as ObtainTokenResponse, issued with Token by default. as_view takes the serializer and the optional token_cls as typed keyword arguments and passes everything else to django as usual, see as_view().

They all set auth = None: they are the very endpoints that check credentials, so auth from the settings must never be required to reach them.

Note

Pass token_cls to as_view when you swap the token model, see Swapping the token model. The default is imported the first time the controller is routed, so projects with their own model never need 'dmr.security.token.app' installed.

Tip

These should be your default for the common cases. They are final, so any custom logic belongs to the reusable controllers below, which leave the bodies and the hooks open for you.

Customizable obtain views#

When the bodies of Ready-to-use obtain views do not match your API, use the two Reusable controllers they are built on:

To use a pre-defined controller, you will need to:

  1. Provide actual types for serializer, request model, and response body. Optionally you can also provide a User model type as the 4th type argument, by default it is AbstractBaseUser

  2. Redefine convert_auth_payload() to convert your request model into the kwargs of django.contrib.auth.authenticate() to authenticate your request

  3. Redefine make_api_response() to return the response in the format of your choice

Run result

$ curl http://127.0.0.1:8000/api/auth/ -X POST -d '{"username": "test_user", "password": "password"}' -H 'Content-Type: application/json'
{"token":"JxRzAZsJPbRXmZut2drklFTnLbMof-ns9GWJko_As8Y"}

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"
      },
      "ObtainTokenPayload": {
        "description": "Payload for default version of an opaque token request body.\n\nIs also used as kwargs for :func:`django.contrib.auth.authenticate`.",
        "properties": {
          "password": {
            "title": "Password",
            "type": "string"
          },
          "username": {
            "title": "Username",
            "type": "string"
          }
        },
        "required": [
          "username",
          "password"
        ],
        "title": "ObtainTokenPayload",
        "type": "object"
      },
      "ObtainTokenResponse": {
        "description": "Default response type for opaque token endpoint.",
        "properties": {
          "token": {
            "title": "Token",
            "type": "string"
          }
        },
        "required": [
          "token"
        ],
        "title": "ObtainTokenResponse",
        "type": "object"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/obtaintokenasynccontroller/": {
      "post": {
        "operationId": "postObtainTokenAsyncControllerApiObtaintokenasynccontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ObtainTokenPayload"
              }
            }
          },
          "description": "Payload for default version of an opaque token request body.\n\nIs also used as kwargs for :func:`django.contrib.auth.authenticate`.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ObtainTokenResponse"
                }
              }
            },
            "description": "OK",
            "headers": {
              "Cache-Control": {
                "description": "Credentials must not be stored in any cache.",
                "required": true,
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Unauthorized"
          },
          "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"
          }
        },
        "summary": "By default tokens are acquired on post."
      }
    }
  }
}

In this example we utilize pre-defined types of request model and response body, only doing the bare minimum with no customizations.

Things that you can customize:

  • Request body format

  • Response body format

  • Token settings, like expiration, algorithm, salt, and secret

  • Token class itself

  • Error messages, see Customizing error messages

  • Error handling, see Error handling

  • Response status code and any other regular controller or endpoint features

Revoking a token#

Tokens can be revoked via helper methods:

Here’s an example with the default model:

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": {
      "token": {
        "description": "Opaque token authentication",
        "in": "header",
        "name": "X-API-Token",
        "type": "apiKey"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/revoketokencontroller/": {
      "delete": {
        "operationId": "deleteRevokeTokenControllerApiRevoketokencontroller",
        "responses": {
          "204": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "null"
                }
              }
            },
            "description": "No Content"
          },
          "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": [
          {
            "token": []
          }
        ]
      },
      "summary": "Revoke the token used to make this request."
    }
  }
}

Cleaning up old tokens#

Auth finds a token by its hash and then checks is_active. Expired and revoked tokens always fail that check, so deleting their rows changes nothing for the client: a 401 stays a 401, it just says “unknown token” now.

Which means:

  • Rows that are still active must stay, including the ones with expires_at set to None, because such tokens never expire

  • Rows with expires_at in the past and rows with revoked_at set can be removed, they can never authenticate anyone again

Deleting a token is not a replacement for revoking it, revoke() is what stops a token that is still active. Cleanup is just housekeeping afterwards, and it is worth doing periodically:

Note that expires_at__lt never matches NULL, so tokens without an expiry date are safe from this query by construction.

Then run this task as a periodic job.

Note

Keep a grace period that matches your auditing needs. Dead rows still tell you when a token was issued, last used, and revoked, which is the kind of thing you want during an incident. For the same reason you might want to archive them somewhere else instead of just deleting them.

If you swapped the token model, adjust the query to the fields your own model has.

Tip

Blocklisted JWT tokens have the same problem and the same solution, see Cleaning up expired tokens.

Django admin#

When 'dmr.security.token.app' is in INSTALLED_APPS, default Token model entries are accessible from the Django admin for viewing, searching, filtering, and revocation.

Note

Token creation is intentionally disabled in the admin. issue() returns the raw token exactly once and an admin form has no way to surface that value. Use issue() or aissue() directly to issue tokens instead.

Active tokens can be revoked individually from the change form, or in bulk using the Revoke selected tokens action from the change list.

Tracking last use#

Auth classes accept an update_last_used flag for tracking when a token was last successfully used. It is opt-in, defaulting to False:

>>> from dmr.security.token import HeaderTokenSyncAuth

>>> HeaderTokenSyncAuth(update_last_used=True)
<dmr.security.token.auth.header.HeaderTokenSyncAuth object at ...>

When enabled, every successful authentication writes last_used_at and updated_at back to the token’s row.

Warning

Enabling this turns every authenticated request into a database write, not just token creation and revocation. On high-traffic endpoints this can meaningfully increase database load.

If you need last-used tracking but want to control the write cost, consider:

  • throttling writes to at most once per interval (for example, only updating if the existing last_used_at is older than a few minutes)

  • writing out-of-band, for example via a task queue, instead of inline in the request path

  • leaving it disabled (the default) and relying on application-level logging or analytics instead

Warning

Token’s last_used_at is not updated atomically. If you need a transaction, you should override authenticate method and wrap it in transaction.atomic(), but it would make the process even slower.

Customizing the User model#

Our Token model links to the User type via customizable setting value:

models.ForeignKey(settings.AUTH_USER_MODEL, related_name='dmr_tokens')

So, if you configure your settings.AUTH_USER_MODEL to be something else than the default user (but, still a subclass of django.contrib.auth.models.AbstractBaseUser), it would just work out of the box.

Swapping the token model#

Note

Swapping the model is an advanced feature, using the default Token model is the correct way in most cases.

Let’s say that you already have some token auth mechanism from some other API framework with existing CustomToken model that you want to continue using, so nothing would change for your users.

This old model might have a completely different structure, different fields, user models, etc. What we care about is that you implement:

  • TokenLikeSync interface for sync auth

  • TokenLikeAsync interface for async auth

  • Both of them, if you need a model that works with sync and async auth, like our default Token does

Note

Custom token models can support custom user models as well. Our interfaces are even generic on the User type, see Using a custom user model below.

Otherwise, you would be required to work with AbstractBaseUser which is the default type parameter for TokenLikeSync and TokenLikeAsync.

Here’s an example of a custom model with sync interface only:

Next, let’s define an auth class with a different model type:

And protect your views with this new auth type:

This way you can keep old tokens and old model for your existing users. API stability is important!

Note

This way you can also have different auth classes that work with different models types, if this is a business requirement you have.

Using a custom user model#

TokenLikeSync and TokenLikeAsync are generic on the User type, so a custom token model can point at any AbstractBaseUser subclass, not just the default User model or basic AbstractBaseUser:

Parametrize both interfaces with it to get a model that works with sync and async auth at the same time:

The auth classes then declare the custom model, and the exact user type variable is inferred from it:

Which is all that is needed to protect both sync and async views:

API Reference#

class dmr.security.token.HeaderTokenSyncAuth(*, header_name: str = 'X-API-Token', prefix: str = '', www_authenticate: bool = True, security_scheme_name: str = 'token', update_last_used: bool = False, token_secret: str | None = None, token_salt: str = 'dmr.security.token', token_algorithm: str = 'sha256')[source]#

Sync opaque token auth; reads from X-API-Token by default.

Added in version 0.12.0.

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

Authenticate via opaque token.

authenticate(request: HttpRequest, raw_token: str) → AbstractBaseUser#

Run all auth pipeline.

check_token(token: TokenLikeSync) → None#

Raise NotAuthenticatedError if the token is not active.

check_user(user: AbstractBaseUser) → None#

Raise NotAuthenticatedError if user account is not active.

get_raw_token(request: HttpRequest) → str | None#

Read the raw token from the request header, stripping any prefix.

get_token(raw_token: str) → TokenLikeSync#

Look up and validate the token from the DB.

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, *, token: TokenLikeSync) → None#

Set current user as authed for this request.

property token_model: type[TokenLikeSync[Any]]#

Return sync token model.

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#

Challenge naming the scheme this auth expects, like Bearer.

Returns None for a custom header_name, because a challenge can only ask the client for the Authorization header. Also returns None for an empty prefix: without one there is no scheme name to build a challenge from.

class dmr.security.token.HeaderTokenAsyncAuth(*, header_name: str = 'X-API-Token', prefix: str = '', www_authenticate: bool = True, security_scheme_name: str = 'token', update_last_used: bool = False, token_secret: str | None = None, token_salt: str = 'dmr.security.token', token_algorithm: str = 'sha256')[source]#

Async opaque token auth; reads from X-API-Token by default.

Added in version 0.12.0.

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

Authenticate via opaque token.

async authenticate(request: HttpRequest, raw_token: str) → AbstractBaseUser#

Run all auth pipeline.

async check_token(token: TokenLikeAsync) → None#

Raise NotAuthenticatedError if the token is not active.

async check_user(user: AbstractBaseUser) → None#

Raise NotAuthenticatedError if user account is not active.

get_raw_token(request: HttpRequest) → str | None#

Read the raw token from the request header, stripping any prefix.

async get_token(raw_token: str) → TokenLikeAsync#

Look up and validate the token from the DB.

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, *, token: TokenLikeAsync) → None#

Set current user as authed for this request.

property token_model: type[TokenLikeAsync[Any]]#

Return sync token model.

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#

Challenge naming the scheme this auth expects, like Bearer.

Returns None for a custom header_name, because a challenge can only ask the client for the Authorization header. Also returns None for an empty prefix: without one there is no scheme name to build a challenge from.

class dmr.security.token.CookieTokenSyncAuth(*, cookie_name: str = 'token', security_scheme_name: str = 'token', csrf_scheme_name: str = 'csrf', update_last_used: bool = False, token_secret: str | None = None, token_salt: str = 'dmr.security.token', token_algorithm: str = 'sha256')[source]#

Sync opaque token auth reading from a cookie.

CSRF is automatically enforced before any other actions.

Added in version 0.12.0.

Changed in version 0.16.0: Fixed how CSRF schema is generated.

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

Enforce CSRF, then authenticate via cookie token.

auth_security_scheme() → SecurityScheme#

Provides a security schema definition.

authenticate(request: HttpRequest, raw_token: str) → AbstractBaseUser#

Run all auth pipeline.

check_token(token: TokenLikeSync) → None#

Raise NotAuthenticatedError if the token is not active.

check_user(user: AbstractBaseUser) → None#

Raise NotAuthenticatedError if user account is not active.

csrf_security_scheme() → SecurityScheme#

Provides the security scheme of the CSRF.

get_raw_token(request: HttpRequest) → str | None#

Read the raw token from a cookie.

get_token(raw_token: str) → TokenLikeSync#

Look up and validate the token from the DB.

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

Declares extra responses for failed auth and failed CSRF checks.

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

Requires the auth scheme and CSRF for unsafe HTTP methods.

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

Provides the auth security scheme together with the CSRF one.

set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, token: TokenLikeSync) → None#

Set current user as authed for this request.

property token_model: type[TokenLikeSync[Any]]#

Return sync token model.

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#

This auth has no challenge to advertise, so this returns None.

A challenge asks the client for the Authorization header, while this auth reads credentials that need CSRF protection instead.

class dmr.security.token.CookieTokenAsyncAuth(*, cookie_name: str = 'token', security_scheme_name: str = 'token', csrf_scheme_name: str = 'csrf', update_last_used: bool = False, token_secret: str | None = None, token_salt: str = 'dmr.security.token', token_algorithm: str = 'sha256')[source]#

Async opaque token auth reading from a cookie.

CSRF is automatically enforced before any other actions.

Added in version 0.12.0.

Changed in version 0.16.0: Fixed how CSRF schema is generated.

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

Enforce CSRF, then authenticate via cookie token.

auth_security_scheme() → SecurityScheme#

Provides a security schema definition.

async authenticate(request: HttpRequest, raw_token: str) → AbstractBaseUser#

Run all auth pipeline.

async check_token(token: TokenLikeAsync) → None#

Raise NotAuthenticatedError if the token is not active.

async check_user(user: AbstractBaseUser) → None#

Raise NotAuthenticatedError if user account is not active.

csrf_security_scheme() → SecurityScheme#

Provides the security scheme of the CSRF.

get_raw_token(request: HttpRequest) → str | None#

Read the raw token from a cookie.

async get_token(raw_token: str) → TokenLikeAsync#

Look up and validate the token from the DB.

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

Declares extra responses for failed auth and failed CSRF checks.

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

Requires the auth scheme and CSRF for unsafe HTTP methods.

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

Provides the auth security scheme together with the CSRF one.

async set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, token: TokenLikeAsync) → None#

Set current user as authed for this request.

property token_model: type[TokenLikeAsync[Any]]#

Return sync token model.

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#

This auth has no challenge to advertise, so this returns None.

A challenge asks the client for the Authorization header, while this auth reads credentials that need CSRF protection instead.

Helpers#

dmr.security.token.request_token(request: HttpRequest, *, strict: Literal[True], sync: Literal[False]) → TokenLikeAsync[source]#
dmr.security.token.request_token(request: HttpRequest, *, strict: Literal[True], sync: Literal[True] = True) → TokenLikeSync
dmr.security.token.request_token(request: HttpRequest, *, strict: bool = False, sync: Literal[False]) → TokenLikeAsync | None
dmr.security.token.request_token(request: HttpRequest, *, strict: bool = False, sync: Literal[True] = True) → TokenLikeSync | None

Return the TokenLike from request, if it was authed with one.

Raises:
  • AttributeError – When strict is passed and request has no token.

  • TypeError – When sync boolean does not match the token interface.

Added in version 0.12.0.

dmr.security.token.token.get_token_hash(raw_token: str, *, secret: str | None, salt: str | None = None, algorithm: str | None = None) → str[source]#

Hash the token value with the secret key.

Parameters:
  • raw_token – Raw string to be hashed.

  • secret – What secret should we use for token hash? Default to settings.SECRET_KEY.

  • salt – What salt should we use for token hash?

  • algorithm – What algorithm should we use for token hash?

Added in version 0.12.0.

dmr.security.token.token.resolve_expiry(expires_at: datetime | EMPTY | None, *, expiration: timedelta | None = None) → datetime | None[source]#

Resolve expiery for optional value.

Added in version 0.12.0.

Interfaces#

class dmr.security.token.token.TokenLikeSync[source]#

Base sync interface for all token models.

This type is generic on the user type. If you want to customize the User object that you are working with, just inherit from a generic version: TokenLikeSync[CustomUser]. Otherwise, you would work with AbstractBaseUser, which is the default type parameter.

See also

Using a custom user model for a complete example.

Added in version 0.12.0.

abstractmethod classmethod find_raw(raw_token: str, *, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → Self | None[source]#

Find token by its hash.

abstractmethod get_user() → _UserT[source]#

Get user that this token belongs to.

abstractmethod classmethod issue(*, user: _UserT, name: str, expires_at: datetime | EMPTY | None = EMPTY, token_size: int | None = None, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → tuple[Self, str][source]#

Create new token.

Parameters:
  • user – User instance to store. Type annotation can be customized via generic type arguments.

  • name – Name of the token.

  • expires_at – When this token expires? Can be one of three: a specific date, None for tokens that do not expire at all, EMPTY to calculate the exiry relative to the current date.

  • token_size – Size of the raw token in chars.

  • token_secret – Secret key to be used for the token hash. Defaults to settings.SECRET_KEY.

  • token_salt – Salt to be used for the token hash.

  • token_algorithm – Salt to be used for the token hash. Defaults to sha256.

abstractmethod mark_used() → None[source]#

Mark this token as used.

abstractmethod revoke(*, at: datetime | None = None) → None[source]#

Revoke this token.

class dmr.security.token.token.TokenLikeAsync[source]#

Base async interface for all token models.

This type is generic on the user type. If you want to customize the User object that you are working with, just inherit from a generic version: TokenLikeAsync[CustomUser]. Otherwise, you would work with AbstractBaseUser, which is the default type parameter.

See also

Using a custom user model for a complete example.

Added in version 0.12.0.

abstractmethod async classmethod afind_raw(raw_token: str, *, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → Self | None[source]#

Async find token by its hash.

abstractmethod async aget_user() → _UserT[source]#

Async get user that this token belongs to.

abstractmethod async classmethod aissue(*, user: _UserT, name: str, expires_at: datetime | EMPTY | None = EMPTY, token_size: int | None = None, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → tuple[Self, str][source]#

Async version of TokenLikeSync.issue().

abstractmethod async amark_used() → None[source]#

Async mark this token as used.

abstractmethod async arevoke(*, at: datetime | None = None) → None[source]#

Async revoke this token.

Ready-to-use views to fetch opaque tokens#

final class dmr.security.token.concrete_views.ObtainTokenSyncController(**kwargs)[source]#

Sync controller to issue an opaque token, ready to be routed.

Takes ObtainTokenPayload and returns ObtainTokenResponse. token_cls defaults to Token, so the only thing it needs is a serializer type:

>>> from dmr.plugins.pydantic import PydanticFastSerializer
>>> from dmr.routing import path
>>> route = path(
...     'login/',
...     ObtainTokenSyncController.as_view(
...         serializer=PydanticFastSerializer,
...     ),
... )

Pass token_cls to as_view() when you swap the token model, see Swapping the token model. The default is imported the first time the controller is routed, so projects with their own model do not need 'dmr.security.token.app' installed.

See ObtainTokenSyncController for all the token settings and hooks it inherits, and subclass that one for any custom logic instead of this controller.

Added in version 0.16.0.

classmethod as_view(*, serializer: type[BaseSerializer] | EMPTY = EMPTY, token_cls: type[TokenLikeSync[_UserT]] | EMPTY = EMPTY, **initkwargs: Any) → Callable[[...], HttpResponseBase][source]#

Route this controller with its required fields filled in.

serializer is required, token_cls replaces the bundled Token model. initkwargs go to django as usual.

convert_auth_payload(payload: ObtainTokenPayload) → ObtainTokenPayload[source]#

Default payload is already what authenticate expects.

make_api_response() → ObtainTokenResponse[source]#

Issue a new token for the user we have just authenticated.

final class dmr.security.token.concrete_views.ObtainTokenAsyncController(**kwargs)[source]#

Async controller to issue an opaque token, ready to be routed.

Async version of ObtainTokenSyncController.

Added in version 0.16.0.

classmethod as_view(*, serializer: type[BaseSerializer] | EMPTY = EMPTY, token_cls: type[TokenLikeAsync[_UserT]] | EMPTY = EMPTY, **initkwargs: Any) → Callable[[...], HttpResponseBase][source]#

Route this controller with its required fields filled in.

serializer is required, token_cls replaces the bundled Token model. initkwargs go to django as usual.

async convert_auth_payload(payload: ObtainTokenPayload) → ObtainTokenPayload[source]#

Default payload is already what aauthenticate expects.

async make_api_response() → ObtainTokenResponse[source]#

Issue a new token for the user we have just authenticated.

Pre-defined views to fetch opaque tokens#

class dmr.security.token.views.ObtainTokenSyncController(**kwargs)[source]#

Sync controller to issue a new opaque token.

token_cls#

Token class to be used.

Type:

type[dmr.security.token.token.TokenLikeSync[dmr.security.token.views._UserT]]

token_secret#

Secret key to be used for the token hash. Defaults to settings.SECRET_KEY.

Type:

str | None

token_salt#

Salt to be used for the token hash.

Type:

str | None

token_algorithm#

Salt to be used for the token hash. Defaults to sha256.

Type:

str | None

token_expiration#

Default token expiration.

Type:

datetime.timedelta

Added in version 0.12.0.

Changed in version 0.15.0: Now using @modify.lazy with the ability to change the spec.

abstractmethod convert_auth_payload(payload: _ObtainTokenT) → ObtainTokenPayload[source]#

Convert your custom payload to kwargs that django supports.

See django.contrib.auth.authenticate() docs on which kwargs it supports.

Basically it needs username and password strings.

issue_token(*, user: _UserT, name: str | None = None, expires_at: datetime | EMPTY | None = EMPTY, token_size: int | None = None, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → str[source]#

Create correct opaque token with all possible customizations.

login(parsed_body: _ObtainTokenT) → _TokenResponseT[source]#

Perform the sync login routine for user.

abstractmethod make_api_response() → _TokenResponseT[source]#

Abstract method to create a response payload.

make_token_name() → str#

Create unique token’s name.

post(parsed_body: ~typing.Annotated[~dmr.security.token.views._ObtainTokenT, <dmr.components.BodyComponent object at 0x7170bff25900>]) → _TokenResponseT[source]#

By default tokens are acquired on post.

class dmr.security.token.views.ObtainTokenAsyncController(**kwargs)[source]#

Sync controller to issue a new opaque token.

token_cls#

Token class to be used.

Type:

type[dmr.security.token.token.TokenLikeAsync[dmr.security.token.views._UserT]]

token_secret#

Secret key to be used for the token hash. Defaults to settings.SECRET_KEY.

Type:

str | None

token_salt#

Salt to be used for the token hash.

Type:

str | None

token_algorithm#

Salt to be used for the token hash. Defaults to sha256.

Type:

str | None

Added in version 0.12.0.

Changed in version 0.15.0: Now using @modify.lazy with the ability to change the spec.

abstractmethod async convert_auth_payload(payload: _ObtainTokenT) → ObtainTokenPayload[source]#

Convert your custom payload to kwargs that django supports.

See django.contrib.auth.authenticate() docs on which kwargs it supports.

Basically it needs username and password strings.

async issue_token(*, user: _UserT, name: str | None = None, expires_at: datetime | EMPTY | None = EMPTY, token_size: int | None = None, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → str[source]#

Create correct opaque token with all possible customizations.

async login(parsed_body: _ObtainTokenT) → _TokenResponseT[source]#

Perform the sync login routine for user.

abstractmethod async make_api_response() → _TokenResponseT[source]#

Abstract method to create a response payload.

make_token_name() → str#

Create unique token’s name.

async post(parsed_body: ~typing.Annotated[~dmr.security.token.views._ObtainTokenT, <dmr.components.BodyComponent object at 0x7170bff25900>]) → _TokenResponseT[source]#

By default tokens are acquired on post.

class dmr.security.token.views.ObtainTokenPayload[source]#

Bases: TypedDict

Payload for default version of an opaque token request body.

Is also used as kwargs for django.contrib.auth.authenticate().

class dmr.security.token.views.ObtainTokenResponse[source]#

Bases: TypedDict

Default response type for opaque token endpoint.

Default app#

final class dmr.security.token.app.models.Token(*args, **kwargs)[source]#

Model representing a DB-backed opaque auth token.

Added in version 0.12.0.

exception DoesNotExist#
exception MultipleObjectsReturned#
async classmethod afind_raw(raw_token: str, *, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → Self | None[source]#

Async find token by its hash.

async aget_user() → AbstractBaseUser[source]#

Async get user that this token belongs to.

async classmethod aissue(*, user: AbstractBaseUser, name: str, expires_at: datetime | EMPTY | None = EMPTY, token_size: int | None = None, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → tuple[Token, str][source]#

Async version of Token.issue().

async amark_used() → None[source]#

Async mark this token as used.

async arevoke(*, at: datetime | None = None) → None[source]#

Async mark this token as revoked.

classmethod find_raw(raw_token: str, *, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → Self | None[source]#

Find token by its hash.

get_user() → AbstractBaseUser[source]#

Get user that this token belongs to.

property is_active: bool#

Return True if the token is neither expired nor revoked.

property is_expired: bool#

Return True if the token has passed its expiry date.

classmethod issue(*, user: AbstractBaseUser, name: str, expires_at: datetime | EMPTY | None = EMPTY, token_size: int | None = None, token_secret: str | None = None, token_salt: str | None = None, token_algorithm: str | None = None) → tuple[Self, str][source]#

Create a new token, returning the token model and raw token data.

mark_used() → None[source]#

Mark this token as used.

revoke(*, at: datetime | None = None) → None[source]#

Mark this token as revoked.