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:
HeaderTokenSyncAuthandHeaderTokenAsyncAuthfor header-based authQueryTokenSyncAuthandQueryTokenAsyncAuthfor query-param based authCookieTokenSyncAuthandCookieTokenAsyncAuthfor cookie-based auth
Example of requiring token auth and accessing
both self.request.user and the current token:
1from django.contrib.auth.models import User
2
3from dmr import Controller
4from dmr.plugins.pydantic import PydanticSerializer
5from dmr.security import AuthenticatedHttpRequest
6from dmr.security.token import HeaderTokenSyncAuth
7
8
9class APIController(Controller[PydanticSerializer]):
10 request: AuthenticatedHttpRequest[User]
11 auth = (HeaderTokenSyncAuth(),)
12
13 def get(self) -> str:
14 # Let's test that `User` is authenticated:
15 assert self.request.user.is_authenticated
16 return 'authed'
17
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {
"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:
dmr.security.token.logic.token_create()/dmr.security.token.logic.token_acreate()to create tokensdmr.security.token.logic.token_revoke()/dmr.security.token.logic.token_arevoke()to revoke tokens
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 creationrevoked_at—Noneuntiltoken_revoke()is called
On each authenticated request, the auth backend:
Hashes the incoming raw token and looks up the matching row. If no row matches, authentication fails with a
401Checks that the token is still active (neither revoked nor expired)
- 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:
1from django.contrib.auth.models import User
2
3from dmr import Controller
4from dmr.plugins.pydantic import PydanticSerializer
5from dmr.security import AuthenticatedHttpRequest
6from dmr.security.django_session import DjangoSessionSyncAuth
7from dmr.security.token.logic import token_create
8
9
10class IssueTokenController(Controller[PydanticSerializer]):
11 """Issue a new API token for the currently authenticated user."""
12
13 request: AuthenticatedHttpRequest[User]
14 auth = (DjangoSessionSyncAuth(),)
15
16 def post(self) -> None:
17 token, raw_token = token_create( # noqa: RUF059
18 user=self.request.user,
19 name='api-key',
20 )
21 # raw_token is only available here - return it to the client now.
22 # Only its hash is stored; it cannot be recovered after this point.
23 # In production, include token.pk, token.name, token.expires_at,
24 # and raw_token in a typed response model.
25
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:
1from django.contrib.auth.models import User
2
3from dmr import Controller
4from dmr.plugins.pydantic import PydanticSerializer
5from dmr.security import AuthenticatedHttpRequest
6from dmr.security.token import HeaderTokenSyncAuth, request_token
7from dmr.security.token.logic import token_revoke
8
9
10class RevokeTokenController(Controller[PydanticSerializer]):
11 """Revoke the token used to make this request."""
12
13 request: AuthenticatedHttpRequest[User]
14 auth = (HeaderTokenSyncAuth(),)
15
16 def delete(self) -> None:
17 token = request_token(self.request, strict=True)
18 token_revoke(token)
19
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_atis 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.
Header¶
GET /api/thing HTTP/1.1
X-API-Token: abc123
Classes: HeaderTokenSyncAuth /
HeaderTokenAsyncAuth.
By default, header auth expects X-API-Token: <raw_token>.
You can customize the header name and prefix to match
other conventions, for example:
# DRF-compatible token auth: Authorization: Token <raw_token>
HeaderTokenSyncAuth(header_name='Authorization', prefix='Token')
# Bearer-style auth: Authorization: Bearer <raw_token>
HeaderTokenSyncAuth(header_name='Authorization', prefix='Bearer')
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-Tokenby default.- __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) Self | None¶
Authenticate via opaque token.
- authenticate(request: HttpRequest, raw_token: str) AbstractBaseUser¶
Run all auth pipeline.
- get_raw_token(request: HttpRequest) str | None[source]¶
Read the raw token from the request header, stripping any prefix.
- 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_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.
- 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-Tokenby 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_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 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_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.
- 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
Refererheaders. PreferHeaderTokenSyncAuthfor 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.
- get_raw_token(request: HttpRequest) str | None[source]¶
Read the raw token from the query string.
- 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_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.
- 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
Refererheaders. PreferHeaderTokenAsyncAuthfor 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_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 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_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.
- 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.CsrfViewMiddlewareis 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.
- get_raw_token(request: HttpRequest) str | None[source]¶
Read the raw token from a cookie.
- 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_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.
- 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.CsrfViewMiddlewareis 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_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 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_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.
- 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.