Token Auth

Opaque token authentication backed by database records.

Note

To use token auth, add 'dmr.security.token' to INSTALLED_APPS and run migrations.

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:

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

Token lifecycle

dmr.security.token.models.Token instances are issued and revoked via dedicated functions sync and async functions:

Creation helpers return (token_instance, raw_token). Only the token hash is stored in the database, the raw token is returned exactly once and never persisted.

Token has two DateTimeField fields that gate validity:

  • expires_at — set once, at creation

  • revoked_atNone until token_revoke() is called

On each authenticated request, the auth backend:

  1. Hashes the incoming raw token and looks up the matching row. If no row matches, authentication fails with a 401

  2. Checks that the token is still active (neither revoked nor expired)

  3. Checks that the associated user account is still active

    (is_active)

If any check fails, authentication fails with a 401 and no token state is changed.

Note

Revoking a token is a write: it sets revoked_at on the row. Expiry needs no write at all, expires_at is set once up front and simply compared against the clock on every lookup from then on.

Successful authentication can also write to the row, see Tracking last use below.

        ---
config:
  theme: forest

---
stateDiagram-v2
    [*] --> Active: token_create / token_acreate
    Active --> Revoked: token_revoke / token_arevoke
    Revoked --> [*]
    

Token states

Issuing a token

Issuing a token has to be gated by some pre-existing trust, not by the token itself, otherwise a client would need a token to get a token. That pre-existing trust is specific to your application, so we don’t ship a built-in way to issue tokens. Call token_create() directly, for example from a Django shell:

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/issuetokencontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postIssuetokencontrollerApiIssuetokencontroller",
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "null"
                }
              }
            },
            "description": "Created"
          },
          "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": []
          }
        ]
      }
    }
  }
}

Important

raw_token is only available here, right after creation. Only its hash is stored, so save it now, it cannot be recovered later.

Note

If you want to issue tokens from an HTTP endpoint, for example a “generate API key” button in a dashboard, gate it behind an auth method other than the token being issued. DjangoSessionSyncAuth is a common choice, since the user already has a session from logging in.

Revoking a token

Tokens can be revoked via helper functions:

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": {
        "deprecated": false,
        "operationId": "deleteRevoketokencontrollerApiRevoketokencontroller",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "null"
                }
              }
            },
            "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": []
          }
        ]
      }
    }
  }
}

Django admin

When 'dmr.security.token' is in INSTALLED_APPS, tokens are accessible from the Django admin for viewing, searching, filtering, and revocation.

Note

Token creation is intentionally disabled in the admin. token_create() returns the raw token exactly once and an admin form has no way to surface that value. Use token_create() 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:

HeaderTokenSyncAuth(update_last_used=True)

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

Choosing a transport

Opaque tokens can be sent by clients in three ways. Pick the transport that matches your client.

Query parameter

GET /api/thing?token=abc123

Classes: QueryTokenSyncAuth / QueryTokenAsyncAuth.

Warning

Query param auth leaks tokens into server logs, browser history, and Referer headers. Prefer header-based auth whenever possible.

API Reference

class dmr.security.token.HeaderTokenSyncAuth(*, header_name: str = 'X-API-Token', prefix: str = '', security_scheme_name: str = 'token', update_last_used: bool = True)[source]

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

__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: Token) 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[source]

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

get_token(raw_token: str) Token

Look up and validate the token from the DB.

mark_token_used(token: Token) None

Persist token usage timestamp after successful authentication.

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.

property security_requirement: dict[str, list[str]]

Provides a security schema usage requirement.

property security_schemes: dict[str, SecurityScheme | Reference]

Provides a security schema definition.

set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, token: Token) None

Set current user as authed for this request.

token_model() type[Token]

Returns the Token model. Override to use a custom model.

class dmr.security.token.HeaderTokenAsyncAuth(*, header_name: str = 'X-API-Token', prefix: str = '', security_scheme_name: str = 'token', update_last_used: bool = True)[source]

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

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: Token) 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[source]

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

async get_token(raw_token: str) Token

Look up and validate the token from the DB.

async mark_token_used(token: Token) None

Persist token usage timestamp after successful authentication.

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.

property security_requirement: dict[str, list[str]]

Provides a security schema usage requirement.

property security_schemes: dict[str, SecurityScheme | Reference]

Provides a security schema definition.

async set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, token: Token) None

Set current user as authed for this request.

token_model() type[Token]

Returns the Token model. Override to use a custom model.

class dmr.security.token.QueryTokenSyncAuth(*, query_param: str = 'token', security_scheme_name: str = 'token', update_last_used: bool = True)[source]

Sync opaque token auth reading from a query string parameter.

Warning

Tokens in query strings appear in server access logs, browser history, and HTTP Referer headers. Prefer HeaderTokenSyncAuth for any context where security is a concern.

__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: Token) 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[source]

Read the raw token from the query string.

get_token(raw_token: str) Token

Look up and validate the token from the DB.

mark_token_used(token: Token) None

Persist token usage timestamp after successful authentication.

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.

property security_requirement: dict[str, list[str]]

Provides a security schema usage requirement.

property security_schemes: dict[str, SecurityScheme | Reference]

Provides a security schema definition.

set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, token: Token) None

Set current user as authed for this request.

token_model() type[Token]

Returns the Token model. Override to use a custom model.

class dmr.security.token.QueryTokenAsyncAuth(*, query_param: str = 'token', security_scheme_name: str = 'token', update_last_used: bool = True)[source]

Async opaque token auth reading from a query string parameter.

Warning

Tokens in query strings appear in server access logs, browser history, and HTTP Referer headers. Prefer HeaderTokenAsyncAuth for any context where security is a concern.

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: Token) 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[source]

Read the raw token from the query string.

async get_token(raw_token: str) Token

Look up and validate the token from the DB.

async mark_token_used(token: Token) None

Persist token usage timestamp after successful authentication.

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.

property security_requirement: dict[str, list[str]]

Provides a security schema usage requirement.

property security_schemes: dict[str, SecurityScheme | Reference]

Provides a security schema definition.

async set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, token: Token) None

Set current user as authed for this request.

token_model() type[Token]

Returns the Token model. Override to use a custom model.

class dmr.security.token.CookieTokenSyncAuth(*, cookie_name: str = 'token', security_scheme_name: str = 'token', update_last_used: bool = True)[source]

Sync opaque token auth reading from a cookie.

CSRF is enforced automatically after a successful token look-up.

Warning

Cookie-based authentication is vulnerable to CSRF attacks in browser-facing contexts. Ensure that django.middleware.csrf.CsrfViewMiddleware is active whenever this auth class is used in a browser-facing application.

__call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) CookieTokenSyncAuth | None[source]

Authenticate via cookie token, then enforce CSRF.

authenticate(request: HttpRequest, raw_token: str) AbstractBaseUser

Run all auth pipeline.

check_token(token: Token) 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[source]

Read the raw token from a cookie.

get_token(raw_token: str) Token

Look up and validate the token from the DB.

mark_token_used(token: Token) None

Persist token usage timestamp after successful authentication.

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

Declare extra responses for cookie auth + CSRF checks.

property security_requirement: dict[str, list[str]]

Provides a security schema usage requirement.

property security_schemes: dict[str, SecurityScheme | Reference]

Provides a security schema definition.

set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, token: Token) None

Set current user as authed for this request.

token_model() type[Token]

Returns the Token model. Override to use a custom model.

class dmr.security.token.CookieTokenAsyncAuth(*, cookie_name: str = 'token', security_scheme_name: str = 'token', update_last_used: bool = True)[source]

Async opaque token auth reading from a cookie.

CSRF is enforced automatically after a successful token look-up.

Warning

Cookie-based authentication is vulnerable to CSRF attacks in browser-facing contexts. Ensure that django.middleware.csrf.CsrfViewMiddleware is active whenever this auth class is used in a browser-facing application.

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

Authenticate via cookie token, then enforce CSRF.

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

Run all auth pipeline.

async check_token(token: Token) 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[source]

Read the raw token from a cookie.

async get_token(raw_token: str) Token

Look up and validate the token from the DB.

async mark_token_used(token: Token) None

Persist token usage timestamp after successful authentication.

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

Declare extra responses for cookie auth + CSRF checks.

property security_requirement: dict[str, list[str]]

Provides a security schema usage requirement.

property security_schemes: dict[str, SecurityScheme | Reference]

Provides a security schema definition.

async set_request_attrs(request: HttpRequest, user: AbstractBaseUser, *, token: Token) None

Set current user as authed for this request.

token_model() type[Token]

Returns the Token model. Override to use a custom model.

dmr.security.token.request_token(request: HttpRequest, *, strict: Literal[True]) Token[source]
dmr.security.token.request_token(request: HttpRequest, *, strict: bool = False) Token | None

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

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

dmr.security.token.logic.token_create(*, user: AbstractBaseUser, name: str, expires_at: datetime | sentinel | None = EMPTY) tuple[Token, str][source]

Create a new token, returning (Token instance, raw token string).

async dmr.security.token.logic.token_acreate(*, user: AbstractBaseUser, name: str, expires_at: datetime | sentinel | None = EMPTY) tuple[Token, str][source]

Async version of token_create().

dmr.security.token.logic.token_revoke(token: Token, *, at: datetime | None = None) Token[source]

Mark this token as revoked.

async dmr.security.token.logic.token_arevoke(token: Token, *, at: datetime | None = None) Token[source]

Async version of token_revoke().

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

Model representing a DB-backed opaque auth token.