JWT Auth#
Docs: https://jwt.io
Important
To use jwt you must install 'django-modern-rest[jwt]' extra.
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 JWT auth in your API, depending on where the token is transferred.
Which one do you need?
Token in headers |
Token in cookies |
|
|---|---|---|
Classes |
||
Best for |
Mobile apps, server-to-server calls, and SPAs that keep the token in memory |
Browser apps where JavaScript must never touch the token at all |
Sent by the browser automatically |
No, the client attaches the header itself |
Yes, on every matching request |
Readable by JavaScript |
Yes, the client stores the token itself |
No, when the cookie is issued with |
If your page gets XSS-ed |
The token can be read and stolen |
The cookie cannot be read, though requests can still be made on the user’s behalf |
CSRF |
Not applicable |
Enforced by us, needs |
Cross-origin setup |
Just send the header |
Needs |
When in doubt, use headers. Reach for cookies when the requirement is specifically “the frontend must not be able to read the token”.
When in doubt, use this as the default way to receive tokens.
Use HeaderJWTSyncAuth for sync views
and HeaderJWTAsyncAuth for async views.
They are also available under their older names,
JWTSyncAuth and JWTAsyncAuth.
You can customize:
Security scheme name, default:
jwtHeader name, default:
AuthorizationHeader value prefix, default:
Bearer
Example, how to use the auth class and how to get self.request.user:
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.jwt import HeaderJWTSyncAuth
7
8
9class APIController(Controller[PydanticSerializer]):
10 request: AuthenticatedHttpRequest[User]
11 auth = (HeaderJWTSyncAuth(),)
12
13 def get(self) -> str:
14 # Let's test that `User` has the correct type:
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": {
"jwt": {
"bearerFormat": "JWT",
"description": "JWT token auth",
"scheme": "Bearer",
"type": "http"
}
}
},
"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",
"headers": {
"WWW-Authenticate": {
"description": "Challenges that the client can use to authenticate this request",
"required": true,
"schema": {
"type": "string"
}
}
}
},
"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": [
{
"jwt": []
}
]
}
}
}
}
Use CookieJWTSyncAuth for sync views
and CookieJWTAsyncAuth for async views.
Unlike the Authorization header, the cookie stores
the encoded token as-is, without any Bearer prefix.
Note
We enforce CSRF for this auth as well. See also: https://docs.djangoproject.com/en/stable/ref/csrf
CSRF is only checked when the cookie is actually present, so that requests without it can still fall through to the next auth in the chain.
You can customize:
Security scheme name, default:
jwtCookie name, default:
access_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.jwt import CookieJWTSyncAuth
7
8
9class APIController(Controller[PydanticSerializer]):
10 request: AuthenticatedHttpRequest[User]
11 auth = (CookieJWTSyncAuth(),)
12
13 def get(self) -> str:
14 # Let's test that `User` has the correct type:
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": {
"jwt": {
"description": "JWT token auth via cookie",
"in": "cookie",
"name": "access_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"
},
"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": [
{
"jwt": []
}
]
}
}
}
}
Custom user models are automatically supported.
Tip
Auth classes are tried in order, so you can accept both transports
at once with auth = (CookieJWTSyncAuth(), HeaderJWTSyncAuth()).
The cookie auth returns None when its cookie is missing,
which lets the header auth run next.
Customizing auth#
JWT Auth supports a lot of customization options:
starting from leeway and claim verification
up to the secret key customization.
See decode()
for more info on all configuration options.
JSON backend#
Token payloads are encoded and decoded with the same JSON backend
we use for parsers and renderers: msgspec when it is installed,
native pure Python json otherwise.
See Alternative JSON backends for the details.
Since JWT auth runs on every authenticated request,
having msgspec installed makes
encode() and
decode() noticeably faster.
Warning
Registered claims are always encoded the same way,
but extras can hold arbitrary values.
Only json-native values there
(str, int, float, bool, None, list, and dict)
are guaranteed to produce identical tokens
with and without msgspec installed.
Other types, like timedelta or set,
are either encoded differently or not supported at all
by the pure Python fallback.
Keep extras json-native when your tokens are issued
and verified by different installs.
Reusing pre-existing views#
We provide several pre-existing views to get auth tokens. So, users won’t have to write tons of boilerplate code.
JWT with access and refresh tokens#
We provide two Reusable controllers to obtain pairs of access and refresh tokens:
ObtainTokensSyncControllerfor sync controllersObtainTokensAsyncControllerfor async controllers
To use them, you will need to:
Provide actual types for serializer, request model, and response body
Redefine
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
1import datetime as dt
2
3from typing_extensions import override
4
5from dmr.plugins.pydantic import PydanticSerializer
6from dmr.security.jwt.views import (
7 ObtainTokensPayload,
8 ObtainTokensResponse,
9 ObtainTokensSyncController,
10)
11
12
13# You can also use `ObtainTokensAsyncController` if needed:
14class ObtainAccessAndRefreshSyncController(
15 ObtainTokensSyncController[
16 PydanticSerializer,
17 ObtainTokensPayload,
18 ObtainTokensResponse,
19 ],
20):
21 @override
22 def convert_auth_payload(
23 self,
24 payload: ObtainTokensPayload,
25 ) -> ObtainTokensPayload:
26 return payload
27
28 @override
29 def make_api_response(self) -> ObtainTokensResponse:
30 now = dt.datetime.now(dt.UTC)
31 return {
32 'access_token': self.create_jwt_token(
33 expiration=now + self.jwt_expiration,
34 token_type='access',
35 ),
36 'refresh_token': self.create_jwt_token(
37 expiration=now + self.jwt_refresh_expiration,
38 token_type='refresh',
39 ),
40 }
41
Run result
$ curl http://127.0.0.1:8000/api/auth/ -X POST -d '{"username": "test_user", "password": "password"}' -H 'Content-Type: application/json'
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzg5MTA4MTc2LCJpYXQiOjE3ODkwMjE3NzYsImp0aSI6ImQwY2U4MzI0ZGU4NTRkOTY5NjI0YTE5OTk1MTEyYzQ0IiwiZXh0cmFzIjp7InR5cGUiOiJhY2Nlc3MifX0.FwI49kw8k8MFBlTLJLnVYab0_SEjo5RTcgPGId3lcMo","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzg5ODg1Nzc2LCJpYXQiOjE3ODkwMjE3NzYsImp0aSI6IjExMjY1NTFiYTY4ODRhNzZhOTFlOGUxMWRiNTJkNjM1IiwiZXh0cmFzIjp7InR5cGUiOiJyZWZyZXNoIn19.pfd6XXyTh4GO5oSAB2Kz57zxh0Se67DTXieByOn4TPU"}
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"
},
"ObtainTokensPayload": {
"description": "Payload for default version of a jwt 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": "ObtainTokensPayload",
"type": "object"
},
"ObtainTokensResponse": {
"description": "Default response type for refresh token endpoint.",
"properties": {
"access_token": {
"title": "Access Token",
"type": "string"
},
"refresh_token": {
"title": "Refresh Token",
"type": "string"
}
},
"required": [
"access_token",
"refresh_token"
],
"title": "ObtainTokensResponse",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/obtainaccessandrefreshsynccontroller/": {
"post": {
"deprecated": false,
"operationId": "postObtainaccessandrefreshsynccontrollerApiObtainaccessandrefreshsynccontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ObtainTokensPayload"
}
}
},
"description": "Payload for default version of a jwt 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/ObtainTokensResponse"
}
}
},
"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
JWT settings
JWT token class to be
JWTokensubclass with custom logicError messages, see Customizing error messages
Error handling, see Error handling
Response status code and any other regular controller or endpoint features
Here’s an example with a lot more customizations:
1import datetime as dt
2
3from typing_extensions import TypedDict, override
4
5from dmr.plugins.pydantic import PydanticSerializer
6from dmr.security.jwt.views import (
7 ObtainTokensPayload,
8 ObtainTokensSyncController,
9)
10
11
12# Custom request and response models:
13class _RequestModel(TypedDict):
14 email: str
15 password: str
16
17
18class _TokensModel(TypedDict):
19 access: str
20 refresh: str
21
22
23class _ResponseModel(TypedDict):
24 auth: _TokensModel
25
26
27# You can also use `ObtainTokensAsyncController` if needed:
28class ObtainAccessAndRefreshSyncController(
29 ObtainTokensSyncController[
30 PydanticSerializer,
31 _RequestModel,
32 _ResponseModel,
33 ],
34):
35 # Customizes the default jwt settings:
36 jwt_issuer = 'my-awesome-company'
37 jwt_algorithm = 'HS512'
38 jwt_audiences = ('dev', 'qa')
39
40 @override
41 def convert_auth_payload(
42 self,
43 payload: _RequestModel,
44 ) -> ObtainTokensPayload:
45 return {'username': payload['email'], 'password': payload['password']}
46
47 @override
48 def make_api_response(self) -> _ResponseModel:
49 now = dt.datetime.now(dt.UTC)
50 access = self.create_jwt_token(
51 expiration=now + self.jwt_expiration,
52 token_type='access',
53 )
54 refresh = self.create_jwt_token(
55 expiration=now + self.jwt_refresh_expiration,
56 token_type='refresh',
57 )
58 return {
59 'auth': {
60 'access': access,
61 'refresh': refresh,
62 },
63 }
64
Run result
$ curl http://127.0.0.1:8000/api/auth/ -X POST -d '{"email": "test_user", "password": "password"}' -H 'Content-Type: application/json'
{"auth":{"access":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzg5MTA4MTc3LCJpYXQiOjE3ODkwMjE3NzcsImlzcyI6Im15LWF3ZXNvbWUtY29tcGFueSIsImF1ZCI6WyJkZXYiLCJxYSJdLCJqdGkiOiJmZTgxOWYzM2JjYzI0ZGQ2ODkwODkwNmM0OGQzY2VkMiIsImV4dHJhcyI6eyJ0eXBlIjoiYWNjZXNzIn19.pxNXKB3tbIWGtYfooW3DGuQUP1U0U0R9GYsNfuFTcj4nBszxZb_e4UBKO5w-MQG_EFDER6LdEVTS2hCw1XmrYQ","refresh":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzg5ODg1Nzc3LCJpYXQiOjE3ODkwMjE3NzcsImlzcyI6Im15LWF3ZXNvbWUtY29tcGFueSIsImF1ZCI6WyJkZXYiLCJxYSJdLCJqdGkiOiI2OGJjZGYyZDNiYmI0MmU1OGQzNjZkMjRjNGE5MGUxOSIsImV4dHJhcyI6eyJ0eXBlIjoicmVmcmVzaCJ9fQ.WBpdMR_aqcNtJwMXzEGKtU_rfLLDxojIVUIEQN-_jyppEMMpBLrMsuZ4oCcS9HHNmB8XVdO4bRpZynjJtDpk-A"}}
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"
},
"_RequestModel": {
"properties": {
"email": {
"title": "Email",
"type": "string"
},
"password": {
"title": "Password",
"type": "string"
}
},
"required": [
"email",
"password"
],
"title": "_RequestModel",
"type": "object"
},
"_ResponseModel": {
"properties": {
"auth": {
"$ref": "#/components/schemas/_TokensModel"
}
},
"required": [
"auth"
],
"title": "_ResponseModel",
"type": "object"
},
"_TokensModel": {
"properties": {
"access": {
"title": "Access",
"type": "string"
},
"refresh": {
"title": "Refresh",
"type": "string"
}
},
"required": [
"access",
"refresh"
],
"title": "_TokensModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/obtainaccessandrefreshsynccontroller/": {
"post": {
"deprecated": false,
"operationId": "postObtainaccessandrefreshsynccontrollerApiObtainaccessandrefreshsynccontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseModel"
}
}
},
"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."
}
}
}
}
This example also provides issuer and audience in the token,
so it can be used together with accepted_issuers and accepted_audiences
configurations of HeaderJWTSyncAuth
to additionally validate aud and iss JWT token claims.
We want to be sure that this class is at the same time:
Easy enough to not write a lot of boilerplate code by default
Customizable enough to be able to change a lot of stuff that can be affected by existing business rules
Always type safe
Refreshing tokens#
Once a user has a refresh token, they can use it to obtain a new pair of access and refresh tokens without re-authenticating. We provide two Reusable controllers for this:
RefreshTokenSyncControllerfor sync controllersRefreshTokenAsyncControllerfor async controllers
To use them, you only need to:
Provide actual types for serializer, request payload, and response body
Redefine
convert_refresh_payload()to extract the refresh token string from your request payloadRedefine
make_api_response()to return the new token pair in the format of your choice
The controller validates that the submitted token:
Is a valid, non-expired JWT signed with the configured secret
Has
extras.type == 'refresh'(i.e. it is a refresh token, not an access token)Belongs to an existing, active user
1import datetime as dt
2
3from typing_extensions import override
4
5from dmr.plugins.pydantic import PydanticSerializer
6from dmr.security.jwt.views import (
7 ObtainTokensResponse,
8 RefreshTokenPayload,
9 RefreshTokenSyncController,
10)
11
12
13# You can also use `RefreshTokenAsyncController` if needed:
14class RefreshSyncController(
15 RefreshTokenSyncController[
16 PydanticSerializer,
17 RefreshTokenPayload,
18 ObtainTokensResponse,
19 ],
20):
21 @override
22 def convert_refresh_payload(self, payload: RefreshTokenPayload) -> str:
23 return payload['refresh_token']
24
25 @override
26 def make_api_response(self) -> ObtainTokensResponse:
27 now = dt.datetime.now(dt.UTC)
28 return {
29 'access_token': self.create_jwt_token(
30 expiration=now + self.jwt_expiration,
31 token_type='access',
32 ),
33 'refresh_token': self.create_jwt_token(
34 expiration=now + self.jwt_refresh_expiration,
35 token_type='refresh',
36 ),
37 }
38
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"
},
"ObtainTokensResponse": {
"description": "Default response type for refresh token endpoint.",
"properties": {
"access_token": {
"title": "Access Token",
"type": "string"
},
"refresh_token": {
"title": "Refresh Token",
"type": "string"
}
},
"required": [
"access_token",
"refresh_token"
],
"title": "ObtainTokensResponse",
"type": "object"
},
"RefreshTokenPayload": {
"description": "Default request body type for the refresh token endpoint.",
"properties": {
"refresh_token": {
"title": "Refresh Token",
"type": "string"
}
},
"required": [
"refresh_token"
],
"title": "RefreshTokenPayload",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/refreshsynccontroller/": {
"post": {
"deprecated": false,
"operationId": "postRefreshsynccontrollerApiRefreshsynccontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RefreshTokenPayload"
}
}
},
"description": "Default request body type for the refresh token endpoint.",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ObtainTokensResponse"
}
}
},
"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": "Refresh tokens on POST."
}
}
}
}
Verifying tokens#
Sometimes you need a dedicated endpoint to check whether an access token is still valid, without accessing any protected resource. We provide two Reusable controllers for this:
VerifyTokenSyncControllerfor sync controllersVerifyTokenAsyncControllerfor async controllers
To use them, you only need to:
Provide actual types for serializer and request payload
Redefine
convert_verify_payload()to extract the access token string from your request payload
The controller validates that the submitted token:
Is a valid, non-expired JWT signed with the configured secret
Has
extras.type == 'access'(i.e. it is an access token, not a refresh one)Belongs to an existing, active user
On success it returns an empty 204 No Content response.
On any validation failure it returns 401 Unauthorized.
1from typing_extensions import override
2
3from dmr.plugins.pydantic import PydanticSerializer
4from dmr.security.jwt.views import VerifyTokenPayload, VerifyTokenSyncController
5
6
7# You can also use `VerifyTokenAsyncController` if needed:
8class VerifySyncController(
9 VerifyTokenSyncController[
10 PydanticSerializer,
11 VerifyTokenPayload,
12 ],
13):
14 @override
15 def convert_verify_payload(self, payload: VerifyTokenPayload) -> str:
16 return payload['access_token']
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"
},
"VerifyTokenPayload": {
"description": "Default request body type for the verify token endpoint.",
"properties": {
"access_token": {
"title": "Access Token",
"type": "string"
}
},
"required": [
"access_token"
],
"title": "VerifyTokenPayload",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/verifysynccontroller/": {
"post": {
"deprecated": false,
"operationId": "postVerifysynccontrollerApiVerifysynccontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VerifyTokenPayload"
}
}
},
"description": "Default request body type for the verify token endpoint.",
"required": true
},
"responses": {
"204": {
"content": {
"application/json": {
"schema": {
"type": "null"
}
}
},
"description": "No Content",
"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": "Verify the token on POST."
}
}
}
}
Blocklisting tokens#
Note
Add 'dmr.security.jwt.blocklist' to the INSTALLED_APPS
if you want to use tokens blocklist.
JWT tokens might be leaked / outdated / etc. There must be a way to make a valid, non-expired token blocked from auth.
To do so, we provide a default Django app to do so. We store blocked tokens in the database and provide an API to add tokens to the blocklist.
Here’s an example:
1from django.contrib.auth.models import User
2
3from dmr import Controller
4from dmr.plugins.pydantic import PydanticSerializer
5from dmr.security import AuthenticatedHttpRequest, request_auth
6from dmr.security.jwt import HeaderJWTAsyncAuth, request_jwt
7from dmr.security.jwt.blocklist import JWTokenBlocklistAsyncMixin
8
9
10class JWTAuthWithBlocklist(JWTokenBlocklistAsyncMixin, HeaderJWTAsyncAuth):
11 """This class will also check that tokens are not blocklisted."""
12
13
14jwt_blocklist_auth = JWTAuthWithBlocklist()
15
16
17class APIController(Controller[PydanticSerializer]):
18 request: AuthenticatedHttpRequest[User]
19 auth = (jwt_blocklist_auth,)
20
21 async def get(self) -> str:
22 # Disable tokens for users with old domain emails:
23 if self.request.user.email.endswith('@old-domain.com'):
24 assert request_auth(self.request) is jwt_blocklist_auth
25 await jwt_blocklist_auth.blocklist(
26 request_jwt(self.request, strict=True),
27 )
28 return 'authed'
29
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": {
"jwt": {
"bearerFormat": "JWT",
"description": "JWT token auth",
"scheme": "Bearer",
"type": "http"
}
}
},
"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",
"headers": {
"WWW-Authenticate": {
"description": "Challenges that the client can use to authenticate this request",
"required": true,
"schema": {
"type": "string"
}
}
}
},
"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": [
{
"jwt": []
}
]
}
}
}
}
We provide two mixin types:
JWTokenBlocklistAsyncMixinfor async authJWTokenBlocklistSyncMixinfor sync auth
If this app is installed, we would provide an admin panel by default.
Important
Both mixins add 'jti' to require_claims of the auth class
they are mixed into, on top of whatever you pass yourself.
Blocklist rows are keyed by jti, so a token without one
can never be blocklisted. Accepting such tokens would mean
that the blocklist is silently bypassed:
the lookup would match no rows and the token would stay valid forever.
We reject them with 401 instead.
If you issue tokens with the controllers we ship, make sure that
make_jwt_id()
returns a value, our default implementation already does.
Cleaning up expired tokens#
The blocklist only answers one question:
is this otherwise valid token still allowed?
When exp of a token is in the past,
decode() rejects it
before we even look into the blocklist.
Which means:
Rows with
expires_atin the future must stay, they are the ones actually blocking tokensRows with
expires_atin the past can be removed, they cannot change any auth decision anymore
Nothing removes them for us, so the table grows forever while storing rows that can never affect auth again. We recommend deleting them with a periodic job:
1import datetime as dt
2from typing import Any, final
3
4from django.core.management.base import BaseCommand
5from typing_extensions import override
6
7from dmr.security.jwt.blocklist.models import BlocklistedJWToken
8
9#: Must be bigger than the `leeway` of all your jwt auth classes.
10GRACE_PERIOD = dt.timedelta(days=1)
11
12
13@final
14class Command(BaseCommand):
15 """Delete blocklist entries of tokens that are already expired."""
16
17 help = 'Delete blocklist entries of tokens that are already expired.'
18
19 @override
20 def handle(self, *args: Any, **options: Any) -> None: # noqa: WPS110
21 """Expired tokens are rejected by `exp` before the blocklist runs."""
22 deleted, _ = BlocklistedJWToken.objects.filter(
23 expires_at__lt=dt.datetime.now(dt.UTC) - GRACE_PERIOD,
24 ).delete()
25 self.stdout.write(f'Removed {deleted} blocklisted tokens')
Then run this task as a periodic job.
Warning
Keep the grace period bigger than the largest leeway
you pass to your auth classes.
With a non-zero leeway a token is still accepted
for that many seconds after exp,
and its blocklist row is still doing real work for that long.
Tip
The same reasoning applies to the opaque
Token model,
see Cleaning up old tokens.
API Reference#
- class dmr.security.jwt.token.JWToken(sub: str, exp: datetime, iat: datetime = <factory>, iss: str | None = None, aud: str | Sequence[str] | None = None, jti: str | None = None, extras: dict[str, ~typing.Any]=<factory>)[source]#
JWT Token DTO.
- exp#
Expiration - datetime for token expiration.
- Type:
- iat#
Issued at - should always be current now.
- Type:
- aud#
Audience - intended audience(s).
- Type:
str | collections.abc.Sequence[str] | None
- extras#
Extra fields that were found on the JWT token. Only json-native values are guaranteed to be encoded identically with and without
msgspecinstalled.
Changed in version 0.15.0: Init-only
leewayargument was removed. Time-based claims are now validated in a single place for each direction:encode()checks that a token can be issued, whiledecode()fully relies onpyjwtand its options.- classmethod decode(encoded_token: str, secret: str, algorithm: str, *, leeway: int = 0, accepted_audiences: str | Sequence[str] | None = None, accepted_issuers: str | Sequence[str] | None = None, require_claims: Sequence[str] | None = None, verify_exp: bool = True, verify_iat: bool = True, verify_jti: bool = True, verify_nbf: bool = True, verify_sub: bool = True, strict_audience: bool = False, enforce_minimum_key_length: bool = True) Self[source]#
Decode a passed in token string and return a Token instance.
- Parameters:
encoded_token – A base64 string containing an encoded JWT.
secret – The secret with which the JWT is encoded.
algorithm – The algorithm used to encode the JWT.
leeway – Number of potential seconds as a clock error for expired tokens.
accepted_audiences – Verify the audience when decoding the token.
accepted_issuers – Verify the issuer when decoding the token.
require_claims – Verify that the given claims are present in the token.
verify_exp – Verify that the value of the
exp(expiration) claim is in the future.verify_iat – Verify that
iat(issued at) claim value is an integer.verify_jti – Check that
jti(JWT ID) claim is a string.verify_nbf – Verify that the value of the
nbf(not before) claim is in the past.verify_sub – Check that
sub(subject) claim is a string.strict_audience – Verify that the value of the
aud(audience) claim is a single value, and not a list of values, and matchesaudienceexactly. Requires the value passed to theaudienceto be a sequence of length 1.enforce_minimum_key_length – Raise an auth error when keys are below minimum recommended length.
- Returns:
A decoded Token instance.
- Raises:
NotAuthenticatedError – If the token is invalid.
Changed in version 0.15.0: Time-based claims are only validated by
pyjwt, we don’t validate them a second time anymore. This means thatleeway,verify_exp, andverify_iatare now respected, and that invalid tokens always raisedmr.exceptions.NotAuthenticatedError.
- classmethod decode_payload(encoded_token: str, secret: str, algorithms: list[str], *, leeway: int, issuer: str | Sequence[str] | None, audience: str | Sequence[str] | None, options: Options | None) dict[str, Any][source]#
Decode and verify the JWT and return its payload.
- encode(secret: str | bytes, algorithm: str, headers: dict[str, Any] | None = None) str[source]#
Encode the token instance into a string.
- Parameters:
secret – The secret with which the JWT is encoded.
algorithm – The algorithm used to encode the JWT.
headers – Optional headers to include in the JWT (e.g., {“kid”: “…”}).
- Returns:
An encoded token string.
- Raises:
JWTokenError – If the token cannot be issued right now (exp/iat validation) or encoding fails. pyjwt errors are wrapped and the original exception is preserved as the cause.
Changed in version 0.15.0:
expandiatare validated here viavalidate_issued_claims(), previously it was done during the instance creation. Encoding failures now raiseJWTokenErrorinstead of the HTTP-layerInternalServerError.
- validate_issued_claims() None[source]#
Ensure that this token makes sense to be issued right now.
Is called by
encode(), override it to change or to extend the checks that we run before signing a token.- Raises:
JWTokenError – If this token cannot be issued right now.
Added in version 0.15.0.
- final exception dmr.security.jwt.token.JWTokenError[source]#
Bases:
ExceptionRaised when a token cannot be created, encoded, or decoded.
This is a semantic error about the token itself: its claims, the signing algorithm, or the key. It is not an HTTP error, because tokens are regularly created outside of any request: in management commands, background tasks, and scripts.
Added in version 0.15.0.
Header auth#
- class dmr.security.jwt.auth.HeaderJWTSyncAuth(*, auth_header: str = 'Authorization', auth_scheme: str = 'Bearer', www_authenticate: bool = True, user_id_field: str = 'pk', algorithm: str = 'HS256', security_scheme_name: str = 'jwt', secret: str | None = None, token_cls: type[JWToken] = <class 'dmr.security.jwt.token.JWToken'>, leeway: int = 0, accepted_audiences: str | Sequence[str] | None = None, accepted_issuers: str | Sequence[str] | None = None, require_claims: Sequence[str] | None = None, verify_expiry: bool = True, verify_issued_at: bool = True, verify_jwt_id: bool = True, verify_not_before: bool = True, verify_subject: bool = True, strict_audience: bool = False, enforce_minimum_key_length: bool = True)[source]#
Sync jwt auth reading the token from a header.
Defaults to
Authorization: Bearer <token>.Added in version 0.15.0: Previously known as
JWTSyncAuth, which is still available as an alias.- __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) Self | None#
Does check for the correct jwt token.
- authenticate(request: HttpRequest, token: JWToken) AbstractBaseUser#
Run all auth pipeline.
- check_auth(user: AbstractBaseUser, token: JWToken) None#
Run extra auth checks, raise if something is wrong.
- claim_from_token(token: JWToken) str#
Return claim value from the token object.
Override this method if you want to change how claim is extracted from token. For example, if you create
emailclaim, it will be stored in.extras.So, you would need to use:
token.extras['email'].
- get_token_from_request(request: HttpRequest) str | None#
Gets the jwt token from the request header.
- prepare_token(request: HttpRequest) JWToken | None#
Fetches JWToken instance from the request.
- 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: JWToken) None#
Set current user as authed for this request.
- split_encoded_token(header: str) str | None#
Splits string like ‘Bearer token’ and returns ‘token’ part.
- property www_authenticate_challenge: str | None#
Challenge naming the scheme this auth expects, like
Bearer.Returns
Nonefor a custom auth_header, because a challenge can only ask the client for theAuthorizationheader.realmis optional for bearer tokens, see RFC 6750 Section 3, and we don’t send it.
- class dmr.security.jwt.auth.HeaderJWTAsyncAuth(*, auth_header: str = 'Authorization', auth_scheme: str = 'Bearer', www_authenticate: bool = True, user_id_field: str = 'pk', algorithm: str = 'HS256', security_scheme_name: str = 'jwt', secret: str | None = None, token_cls: type[JWToken] = <class 'dmr.security.jwt.token.JWToken'>, leeway: int = 0, accepted_audiences: str | Sequence[str] | None = None, accepted_issuers: str | Sequence[str] | None = None, require_claims: Sequence[str] | None = None, verify_expiry: bool = True, verify_issued_at: bool = True, verify_jwt_id: bool = True, verify_not_before: bool = True, verify_subject: bool = True, strict_audience: bool = False, enforce_minimum_key_length: bool = True)[source]#
Async jwt auth reading the token from a header.
Defaults to
Authorization: Bearer <token>.Added in version 0.15.0: Previously known as
JWTAsyncAuth, which is still available as an alias.- async __call__(endpoint: Endpoint, controller: Controller[BaseSerializer]) Self | None#
Does check for the correct jwt token.
- async authenticate(request: HttpRequest, token: JWToken) AbstractBaseUser#
Run all auth pipeline.
- async check_auth(user: AbstractBaseUser, token: JWToken) None#
Run extra auth checks, raise if something is wrong.
- claim_from_token(token: JWToken) str#
Return claim value from the token object.
Override this method if you want to change how claim is extracted from token. For example, if you create
emailclaim, it will be stored in.extras.So, you would need to use:
token.extras['email'].
- get_token_from_request(request: HttpRequest) str | None#
Gets the jwt token from the request header.
- prepare_token(request: HttpRequest) JWToken | None#
Fetches JWToken instance from the request.
- 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: JWToken) None#
Set current user as authed for this request.
- split_encoded_token(header: str) str | None#
Splits string like ‘Bearer token’ and returns ‘token’ part.
- property www_authenticate_challenge: str | None#
Challenge naming the scheme this auth expects, like
Bearer.Returns
Nonefor a custom auth_header, because a challenge can only ask the client for theAuthorizationheader.realmis optional for bearer tokens, see RFC 6750 Section 3, and we don’t send it.
Note
Since version 0.15.0 JWTSyncAuth and JWTAsyncAuth
are kept as aliases of
HeaderJWTSyncAuth and
HeaderJWTAsyncAuth.
Existing code keeps working unchanged.
They are soft-deprecated and will be removed before the 1.0.0 release.
Do not use them.
Helpers#
- dmr.security.jwt.auth.request_jwt(request: HttpRequest, *, strict: Literal[True]) JWToken[source]#
- dmr.security.jwt.auth.request_jwt(request: HttpRequest, *, strict: bool = False) JWToken | None
Returns a JWToken from request, if it was authed with it.
When strict is passed and request has no jwt token, we raise
AttributeError.
Pre-defined views to fetch JWT tokens#
- class dmr.security.jwt.views.ObtainTokensSyncController(**kwargs)[source]#
Sync controller to get access and refresh tokens.
- jwt_audiences#
String or sequence of string of audiences for JWT token.
- jwt_issuer#
String of who issued this JWT token.
- jwt_algorithm#
Default algorithm to use for token signing.
- jwt_expiration#
Default access token expiration timedelta.
- jwt_refresh_expiration#
Default refresh token expiration timedelta.
- jwt_secret#
Alternative token secret for signing. By default uses
secret.SECRET_KEY
- jwt_token_cls#
Possible custom JWT token class.
See also
https://pyjwt.readthedocs.io/en/stable for all the JWT terms and options explanation.
- abstractmethod convert_auth_payload(payload: _ObtainTokensT) ObtainTokensPayload[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.
- create_jwt_token(*, expiration: datetime | None = None, token_type: Literal['access', 'refresh'] | None = None, subject: str | None = None, issuer: str | None = None, audiences: str | Sequence[str] | None = None, jwt_id: str | None = None, secret: str | None = None, algorithm: str | None = None, token_headers: dict[str, Any] | None = None) str#
Create correct jwt token of a given expiration and token_type.
- login(parsed_body: _ObtainTokensT) _TokensResponseT[source]#
Perform the sync login routine for user.
- class dmr.security.jwt.views.ObtainTokensAsyncController(**kwargs)[source]#
Async controller to get access and refresh tokens.
- jwt_audiences#
String or sequence of string of audiences for JWT token.
- jwt_issuer#
String of who issued this JWT token.
- jwt_algorithm#
Default algorithm to use for token signing.
- jwt_expiration#
Default token expiration timedelta.
- jwt_refresh_expiration#
Default refresh token expiration timedelta.
- jwt_secret#
Alternative token secret for signing. By default uses
secret.SECRET_KEY
- jwt_token_cls#
Possible custom JWT token class.
See also
https://pyjwt.readthedocs.io/en/stable for all the JWT terms and options explanation.
- abstractmethod async convert_auth_payload(payload: _ObtainTokensT) ObtainTokensPayload[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.
- create_jwt_token(*, expiration: datetime | None = None, token_type: Literal['access', 'refresh'] | None = None, subject: str | None = None, issuer: str | None = None, audiences: str | Sequence[str] | None = None, jwt_id: str | None = None, secret: str | None = None, algorithm: str | None = None, token_headers: dict[str, Any] | None = None) str#
Create correct jwt token of a given expiration and token_type.
- async login(parsed_body: _ObtainTokensT) _TokensResponseT[source]#
Perform the async login routine for user.
- class dmr.security.jwt.views.ObtainTokensPayload[source]#
Bases:
TypedDictPayload for default version of a jwt request body.
Is also used as kwargs for
django.contrib.auth.authenticate().
- class dmr.security.jwt.views.ObtainTokensResponse[source]#
Bases:
TypedDictDefault response type for refresh token endpoint.
- class dmr.security.jwt.views.RefreshTokenSyncController(**kwargs)[source]#
Sync controller to refresh access and refresh tokens.
Accepts a refresh token in the request body, validates it, loads the user, and calls
make_api_response()to build the response.- jwt_user_id_field#
User model field matched against
token.sub. Defaults to'pk'.
- jwt_audiences#
String or sequence of string of audiences for JWT token.
- jwt_issuer#
String of who issued this JWT token.
- jwt_algorithm#
Default algorithm to use for token signing.
- jwt_expiration#
Default token expiration timedelta.
- jwt_refresh_expiration#
Default refresh token expiration timedelta.
- jwt_secret#
Alternative token secret for signing. By default uses
secret.SECRET_KEY
- jwt_token_cls#
Possible custom JWT token class.
- abstractmethod convert_refresh_payload(payload: _RefreshTokensT) str[source]#
Extract the refresh token string from the request payload.
- create_jwt_token(*, expiration: datetime | None = None, token_type: Literal['access', 'refresh'] | None = None, subject: str | None = None, issuer: str | None = None, audiences: str | Sequence[str] | None = None, jwt_id: str | None = None, secret: str | None = None, algorithm: str | None = None, token_headers: dict[str, Any] | None = None) str#
Create correct jwt token of a given expiration and token_type.
- abstractmethod make_api_response() _TokensResponseT[source]#
Build the token pair response after a successful refresh.
- class dmr.security.jwt.views.RefreshTokenAsyncController(**kwargs)[source]#
Async controller to refresh access and refresh tokens.
Accepts a refresh token in the request body, validates it, loads the user, and calls
make_api_response()to build the response.- jwt_user_id_field#
User model field matched against
token.sub. Defaults to'pk'.
- jwt_audiences#
String or sequence of string of audiences for JWT token.
- jwt_issuer#
String of who issued this JWT token.
- jwt_algorithm#
Default algorithm to use for token signing.
- jwt_expiration#
Default token expiration timedelta.
- jwt_refresh_expiration#
Default refresh token expiration timedelta.
- jwt_secret#
Alternative token secret for signing. By default uses
secret.SECRET_KEY
- jwt_token_cls#
Possible custom JWT token class.
- abstractmethod async convert_refresh_payload(payload: _RefreshTokensT) str[source]#
Extract the refresh token string from the request payload.
- create_jwt_token(*, expiration: datetime | None = None, token_type: Literal['access', 'refresh'] | None = None, subject: str | None = None, issuer: str | None = None, audiences: str | Sequence[str] | None = None, jwt_id: str | None = None, secret: str | None = None, algorithm: str | None = None, token_headers: dict[str, Any] | None = None) str#
Create correct jwt token of a given expiration and token_type.
- abstractmethod async make_api_response() _TokensResponseT[source]#
Build the token pair response after a successful refresh.
- class dmr.security.jwt.views.RefreshTokenPayload[source]#
Bases:
TypedDictDefault request body type for the refresh token endpoint.
- class dmr.security.jwt.views.VerifyTokenSyncController(**kwargs)[source]#
Sync controller to verify an access token.
Accepts an access token in the request body, decodes and validates it, ensures it is an access token (not a refresh token), and confirms that the token subject belongs to an existing, active user.
Returns an empty
204 No Contentresponse when the token is valid.- jwt_user_id_field#
User model field matched against
token.sub. Defaults to'pk'.
- jwt_audiences#
String or sequence of string of audiences for JWT token.
- jwt_issuer#
String of who issued this JWT token.
- jwt_algorithm#
Default algorithm to use for token signing.
- jwt_expiration#
Default token expiration timedelta.
- jwt_refresh_expiration#
Default refresh token expiration timedelta.
- jwt_secret#
Alternative token secret for signing. By default uses
secret.SECRET_KEY
- jwt_token_cls#
Possible custom JWT token class.
- check_auth(user: Any) None[source]#
Run extra checks on the token’s user, raise if something is off.
- abstractmethod convert_verify_payload(payload: _VerifyTokenT) str[source]#
Extract the access token string from the request payload.
- create_jwt_token(*, expiration: datetime | None = None, token_type: Literal['access', 'refresh'] | None = None, subject: str | None = None, issuer: str | None = None, audiences: str | Sequence[str] | None = None, jwt_id: str | None = None, secret: str | None = None, algorithm: str | None = None, token_headers: dict[str, Any] | None = None) str#
Create correct jwt token of a given expiration and token_type.
- get_user(token: JWToken) AbstractBaseUser[source]#
Fetch user by token.
- class dmr.security.jwt.views.VerifyTokenAsyncController(**kwargs)[source]#
Async controller to verify an access token.
Accepts an access token in the request body, decodes and validates it, ensures it is an access token (not a refresh token), and confirms that the token subject belongs to an existing, active user.
Returns an empty
204 No Contentresponse when the token is valid.- jwt_user_id_field#
User model field matched against
token.sub. Defaults to'pk'.
- jwt_audiences#
String or sequence of string of audiences for JWT token.
- jwt_issuer#
String of who issued this JWT token.
- jwt_algorithm#
Default algorithm to use for token signing.
- jwt_expiration#
Default token expiration timedelta.
- jwt_refresh_expiration#
Default refresh token expiration timedelta.
- jwt_secret#
Alternative token secret for signing. By default uses
secret.SECRET_KEY
- jwt_token_cls#
Possible custom JWT token class.
- async check_auth(user: Any) None[source]#
Run extra checks on the token’s user, raise if something is off.
- abstractmethod async convert_verify_payload(payload: _VerifyTokenT) str[source]#
Extract the access token string from the request payload.
- create_jwt_token(*, expiration: datetime | None = None, token_type: Literal['access', 'refresh'] | None = None, subject: str | None = None, issuer: str | None = None, audiences: str | Sequence[str] | None = None, jwt_id: str | None = None, secret: str | None = None, algorithm: str | None = None, token_headers: dict[str, Any] | None = None) str#
Create correct jwt token of a given expiration and token_type.
- async get_user(token: JWToken) AbstractBaseUser[source]#
Fetch user by token.
Blocklist app#
- final class dmr.security.jwt.blocklist.models.BlocklistedJWToken(*args, **kwargs)[source]#
Model for Blocklisted token.
- exception DoesNotExist#
- exception MultipleObjectsReturned#
- class dmr.security.jwt.blocklist.auth.JWTokenBlocklistSyncMixin(*args, **kwargs)[source]#
Sync mixin for working with tokens blocklist.