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:
tokenHeader name, default:
X-API-TokenHeader value prefix, default:
''
1from typing import Final
2
3from django.contrib.auth.models import User
4
5from dmr import Controller
6from dmr.plugins.pydantic import PydanticFastSerializer
7from dmr.security import AuthenticatedHttpRequest
8from dmr.security.token import HeaderTokenSyncAuth
9
10token_auth: Final = HeaderTokenSyncAuth()
11
12
13class APIController(Controller[PydanticFastSerializer]):
14 request: AuthenticatedHttpRequest[User]
15 auth = (token_auth,)
16
17 def get(self) -> str:
18 assert self.request.user.is_authenticated
19 return 'authed'
20
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": []
}
]
}
}
}
}
Use CookieTokenSyncAuth and
CookieTokenAsyncAuth
for cookie-based auth.
Note
We enforce CSRF for this auth as well. See also: https://docs.djangoproject.com/en/6.0/ref/csrf
You can customize:
Security scheme name, default:
tokenCookie name, default:
token
1from django.contrib.auth.models import User
2
3from dmr import Controller
4from dmr.plugins.pydantic import PydanticFastSerializer
5from dmr.security import AuthenticatedHttpRequest
6from dmr.security.token import CookieTokenSyncAuth
7
8
9class APIController(Controller[PydanticFastSerializer]):
10 request: AuthenticatedHttpRequest[User]
11 auth = (CookieTokenSyncAuth(),)
12
13 def get(self) -> str:
14 assert self.request.user.is_authenticated
15 return 'authed'
16
Run result
$ curl http://127.0.0.1:8000/api/users/ -X GET --cookie token=XMylfxW1WwVB5AlUXpZkgl9Ir4BHHr-M1DI7i1Ue_5M
"authed"
$ curl http://127.0.0.1:8000/api/users/ -D - -X GET --cookie token=wrong-token
HTTP/1.1 401 Unauthorized
date: Mon, 05 Oct 2026 11:16:31 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": {
"csrf": {
"description": "CSRF protection",
"in": "cookie",
"name": "csrftoken",
"type": "apiKey"
},
"token": {
"description": "Opaque token authentication via cookie",
"in": "cookie",
"name": "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:
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:
ObtainTokenSyncControllerfor sync viewsObtainTokenAsyncControllerfor async views
Pass the serializer to as_view in your urls,
there is no view code at all:
1from dmr.openapi import build_schema
2from dmr.openapi.views import OpenAPIJsonView
3from dmr.plugins.pydantic import PydanticFastSerializer
4from dmr.routing import Router, path
5from dmr.security.token import concrete_views
6
7router = Router(
8 'api/',
9 [
10 # You can also use `ObtainTokenAsyncController` if needed:
11 path(
12 'auth/',
13 concrete_views.ObtainTokenSyncController.as_view(
14 serializer=PydanticFastSerializer,
15 ),
16 name='obtain_token',
17 ),
18 ],
19)
20
21urlpatterns = [
22 router.to_urlpatterns(namespace='api'),
23 path(
24 'docs/openapi.json/',
25 OpenAPIJsonView.as_view(build_schema(router)),
26 name='openapi_json',
27 ),
28]
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:
Provide actual types for serializer, request model, and response body. Optionally you can also provide a
Usermodel type as the 4th type argument, by default it isAbstractBaseUserRedefine
convert_auth_payload()to convert your request model into the kwargs ofdjango.contrib.auth.authenticate()to authenticate your requestRedefine
make_api_response()to return the response in the format of your choice
1from typing_extensions import override
2
3from dmr.plugins.pydantic import PydanticFastSerializer
4from dmr.security.token import views
5from dmr.security.token.app.models import Token
6
7
8# You can also use `ObtainTokenSyncController` if needed:
9class ObtainTokenAsyncController(
10 views.ObtainTokenAsyncController[
11 PydanticFastSerializer,
12 views.ObtainTokenPayload,
13 views.ObtainTokenResponse,
14 ],
15):
16 # Specifying token_cls is required:
17 token_cls = Token
18 # These configurations are optional:
19 token_algorithm = 'sha512'
20
21 @override
22 async def convert_auth_payload(
23 self,
24 payload: views.ObtainTokenPayload,
25 ) -> views.ObtainTokenPayload:
26 return payload
27
28 @override
29 async def make_api_response(self) -> views.ObtainTokenResponse:
30 assert self.request.user.is_authenticated
31 return {'token': await self.issue_token(user=self.request.user)}
32
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, andsecretToken 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:
1from http import HTTPStatus
2
3from django.contrib.auth.models import User
4
5from dmr import Controller, modify
6from dmr.plugins.pydantic import PydanticFastSerializer
7from dmr.security import AuthenticatedHttpRequest
8from dmr.security.token import HeaderTokenSyncAuth, request_token
9
10
11class RevokeTokenController(Controller[PydanticFastSerializer]):
12 """Revoke the token used to make this request."""
13
14 request: AuthenticatedHttpRequest[User]
15 auth = (HeaderTokenSyncAuth(),)
16
17 @modify(status_code=HTTPStatus.NO_CONTENT)
18 def delete(self) -> None:
19 token = request_token(self.request, strict=True)
20 token.revoke()
21
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_atset toNone, because such tokens never expireRows with
expires_atin the past and rows withrevoked_atset 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:
1import datetime as dt
2from typing import Any, Final, final
3
4from django.core.management.base import BaseCommand
5from django.db import models
6from typing_extensions import override
7
8from dmr.security.token.app.models import Token
9
10#: How long dead tokens are kept around for auditing.
11GRACE_PERIOD: Final = dt.timedelta(days=30)
12
13
14@final
15class Command(BaseCommand):
16 """Delete tokens that cannot authenticate anyone anymore."""
17
18 help = 'Delete expired and revoked tokens.'
19
20 @override
21 def handle(self, *args: Any, **options: Any) -> None: # noqa: WPS110
22 """Tokens with `expires_at=None` never expire, `__lt` skips them."""
23 cutoff = dt.datetime.now(dt.UTC) - GRACE_PERIOD
24 deleted, _ = Token.objects.filter(
25 models.Q(expires_at__lt=cutoff) | models.Q(revoked_at__lt=cutoff),
26 ).delete()
27 self.stdout.write(f'Removed {deleted} tokens')
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_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
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:
TokenLikeSyncinterface for sync authTokenLikeAsyncinterface for async authBoth of them, if you need a model that works with sync and async auth, like our default
Tokendoes
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:
1import datetime as dt
2import secrets
3from typing import Final, Self, final
4
5from django.contrib.auth.models import User
6from django.db import models
7from typing_extensions import override
8
9from dmr.security.token.token import (
10 TokenLikeSync,
11 get_token_hash,
12 resolve_expiry,
13)
14from dmr.types import EMPTY
15
16_SALTED_TOKEN_SIZE: Final = 64
17
18
19@final
20class CustomToken(TokenLikeSync[User], models.Model):
21 """Custom model representing a DB-backed opaque auth token."""
22
23 owner = models.ForeignKey(
24 User,
25 on_delete=models.CASCADE,
26 related_name='custom_tokens',
27 )
28 token_hash = models.CharField(max_length=_SALTED_TOKEN_SIZE, unique=True)
29 expires_at = models.DateTimeField(null=True, blank=True)
30 revoked_at = models.DateTimeField(null=True, blank=True)
31 created_at = models.DateTimeField(auto_now_add=True)
32
33 @property
34 @override
35 def is_expired(self) -> bool:
36 """Return True if the token has passed its expiry date."""
37 if self.expires_at is None:
38 return False
39 return dt.datetime.now(dt.UTC) >= self.expires_at
40
41 @property
42 @override
43 def is_active(self) -> bool:
44 """Return True if the token is neither expired nor revoked."""
45 return not self.is_expired and self.revoked_at is None
46
47 @override
48 def get_user(self) -> User:
49 """Get user that this token belongs to."""
50 return self.owner
51
52 @override
53 def mark_used(self) -> None:
54 """Mark this token as used."""
55 # Not supported in this example, does nothing
56
57 @override
58 def revoke(
59 self,
60 *,
61 at: dt.datetime | None = None,
62 ) -> None:
63 """Mark this token as revoked."""
64 self.revoked_at = at or dt.datetime.now(dt.UTC)
65 self.save(update_fields=['revoked_at'])
66
67 @classmethod
68 @override
69 def issue( # noqa: WPS211
70 cls,
71 *,
72 user: User,
73 name: str, # unused
74 expires_at: dt.datetime | EMPTY | None = EMPTY,
75 token_size: int | None = None,
76 token_secret: str | None = None,
77 token_salt: str | None = None,
78 token_algorithm: str | None = None,
79 ) -> tuple[Self, str]:
80 """Create a new token, returning the token model and raw token data."""
81 raw_token = secrets.token_urlsafe(token_size)
82 token = CustomToken.objects.create(
83 owner=user,
84 token_hash=get_token_hash(
85 raw_token,
86 secret=token_secret,
87 salt=token_salt,
88 algorithm=token_algorithm,
89 ),
90 expires_at=resolve_expiry(expires_at),
91 )
92 return token, raw_token
93
94 @classmethod
95 @override
96 def find_raw(
97 cls,
98 raw_token: str,
99 *,
100 token_secret: str | None = None,
101 token_salt: str | None = None,
102 token_algorithm: str | None = None,
103 ) -> Self | None:
104 """Find token by its hash."""
105 token_hash = get_token_hash(
106 raw_token,
107 secret=token_secret,
108 salt=token_salt,
109 algorithm=token_algorithm,
110 )
111 return (
112 CustomToken.objects
113 .select_related('owner')
114 .filter(token_hash=token_hash)
115 .first()
116 )
Next, let’s define an auth class with a different model type:
1from typing import final
2
3from django.contrib.auth.models import User
4from typing_extensions import override
5
6from dmr.security.token import HeaderTokenSyncAuth
7from dmr.security.token.token import TokenLikeSync
8from server.apps.token_auth.models import CustomToken
9
10
11@final
12class HeaderCustomTokenSyncAuth(HeaderTokenSyncAuth):
13 @property
14 @override # Note, that we can also specify custom `User` model:
15 def token_model(self) -> type[TokenLikeSync[User]]:
16 return CustomToken
And protect your views with this new auth type:
1from typing import final
2
3import pydantic
4
5from dmr import Controller
6from dmr.plugins.pydantic import PydanticSerializer
7from dmr.security.token import HeaderTokenSyncAuth
8from server.apps.token_auth.auth import HeaderCustomTokenSyncAuth
9
10
11@final
12class _TokenOwner(pydantic.BaseModel):
13 username: str
14 email: str
15 is_active: bool
16
17
18@final
19class ControllerWithTokenSyncAuth(Controller[PydanticSerializer]):
20 auth = (HeaderCustomTokenSyncAuth(),)
21
22 def post(self) -> _TokenOwner:
23 return _TokenOwner.model_validate(
24 self.request.user,
25 from_attributes=True,
26 )
27
28
29@final
30class ControllerWithDefaultTokenSyncAuth(Controller[PydanticSerializer]):
31 """Reads tokens of the bundled model, every setting at its default."""
32
33 auth = (HeaderTokenSyncAuth(),)
34
35 def get(self) -> _TokenOwner:
36 return _TokenOwner.model_validate(
37 self.request.user,
38 from_attributes=True,
39 )
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:
1from typing import Final, final
2
3from django.contrib.auth.base_user import AbstractBaseUser
4from django.db import models
5
6_USERNAME_SIZE: Final = 150
7
8
9@final
10class ApiUser(AbstractBaseUser):
11 """Custom user model, it is not ``settings.AUTH_USER_MODEL``."""
12
13 username = models.CharField(max_length=_USERNAME_SIZE, unique=True)
14 is_active = models.BooleanField(default=True)
15
16 USERNAME_FIELD = 'username' # noqa: WPS115
Parametrize both interfaces with it to get a model that works with sync and async auth at the same time:
1import datetime as dt
2import secrets
3from typing import Final, Self, final
4
5from django.db import models
6from typing_extensions import override
7
8from dmr.security.token.token import (
9 TokenLikeAsync,
10 TokenLikeSync,
11 get_token_hash,
12 resolve_expiry,
13)
14from dmr.types import EMPTY
15from server.apps.token_custom_user.models.user import ApiUser
16
17_TOKEN_HASH_SIZE: Final = 64
18_REVOKE_FIELDS: Final = ('revoked_at', 'updated_at')
19_LAST_USED_FIELDS: Final = ('last_used_at', 'updated_at')
20
21
22@final
23class ApiToken( # noqa: WPS214
24 TokenLikeSync[ApiUser],
25 TokenLikeAsync[ApiUser],
26 models.Model,
27):
28 """Custom token model with both sync and async interfaces."""
29
30 owner = models.ForeignKey(
31 ApiUser,
32 on_delete=models.CASCADE,
33 related_name='api_tokens',
34 )
35 token_hash = models.CharField(max_length=_TOKEN_HASH_SIZE, unique=True)
36 expires_at = models.DateTimeField(null=True, blank=True)
37 revoked_at = models.DateTimeField(null=True, blank=True)
38 last_used_at = models.DateTimeField(null=True, blank=True)
39 created_at = models.DateTimeField(auto_now_add=True)
40 updated_at = models.DateTimeField(auto_now=True)
41
42 # Interfaces implementation
The auth classes then declare the custom model, and the exact user type variable is inferred from it:
1from typing import final
2
3from typing_extensions import override
4
5from dmr.security.token import HeaderTokenAsyncAuth, HeaderTokenSyncAuth
6from dmr.security.token.token import TokenLikeAsync, TokenLikeSync
7from server.apps.token_custom_user.models import ApiToken, ApiUser
8
9
10@final
11class HeaderApiTokenSyncAuth(HeaderTokenSyncAuth):
12 @property
13 @override # Note, that both the token and the `User` model are custom:
14 def token_model(self) -> type[TokenLikeSync[ApiUser]]:
15 return ApiToken
16
17
18@final
19class HeaderApiTokenAsyncAuth(HeaderTokenAsyncAuth):
20 @property
21 @override
22 def token_model(self) -> type[TokenLikeAsync[ApiUser]]:
23 return ApiToken
Which is all that is needed to protect both sync and async views:
1from typing import final
2
3import pydantic
4
5from dmr import Controller
6from dmr.plugins.pydantic import PydanticSerializer
7from server.apps.token_custom_user.auth import (
8 HeaderApiTokenAsyncAuth,
9 HeaderApiTokenSyncAuth,
10)
11
12
13@final
14class _ApiUserInfo(pydantic.BaseModel):
15 username: str
16 is_active: bool
17
18
19@final
20class ControllerWithApiTokenSyncAuth(Controller[PydanticSerializer]):
21 auth = (HeaderApiTokenSyncAuth(update_last_used=True),)
22
23 def get(self) -> _ApiUserInfo:
24 return _ApiUserInfo.model_validate(
25 self.request.user,
26 from_attributes=True,
27 )
28
29
30@final
31class ControllerWithApiTokenAsyncAuth(Controller[PydanticSerializer]):
32 auth = (HeaderApiTokenAsyncAuth(update_last_used=True),)
33
34 async def get(self) -> _ApiUserInfo:
35 return _ApiUserInfo.model_validate(
36 self.request.user,
37 from_attributes=True,
38 )
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-Tokenby 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.
- 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
Nonefor a custom header_name, because a challenge can only ask the client for theAuthorizationheader. Also returnsNonefor 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-Tokenby 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
Nonefor a custom header_name, because a challenge can only ask the client for theAuthorizationheader. Also returnsNonefor 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.
- 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.
- 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.
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.
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
Userobject that you are working with, just inherit from a generic version:TokenLikeSync[CustomUser]. Otherwise, you would work withAbstractBaseUser, 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 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 –
Userinstance 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,
Nonefor tokens that do not expire at all,EMPTYto 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.
- 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
Userobject that you are working with, just inherit from a generic version:TokenLikeAsync[CustomUser]. Otherwise, you would work withAbstractBaseUser, 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 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().
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
ObtainTokenPayloadand returnsObtainTokenResponse.token_clsdefaults toToken, 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_clstoas_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
ObtainTokenSyncControllerfor 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
Tokenmodel. initkwargs go to django as usual.
- convert_auth_payload(payload: ObtainTokenPayload) ObtainTokenPayload[source]#
Default payload is already what
authenticateexpects.
- 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
Tokenmodel. initkwargs go to django as usual.
- async convert_auth_payload(payload: ObtainTokenPayload) ObtainTokenPayload[source]#
Default payload is already what
aauthenticateexpects.
- 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_expiration#
Default token expiration.
- Type:
Added in version 0.12.0.
Changed in version 0.15.0: Now using
@modify.lazywith 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
usernameandpasswordstrings.
- 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.
- 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
Added in version 0.12.0.
Changed in version 0.15.0: Now using
@modify.lazywith 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
usernameandpasswordstrings.
- 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.
- class dmr.security.token.views.ObtainTokenPayload[source]#
Bases:
TypedDictPayload for default version of an opaque token request body.
Is also used as kwargs for
django.contrib.auth.authenticate().
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().
- 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.
- 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.