Django and DRF integration for AWS Cognito machine-to-machine bearer token authentication.
Project description
django-cognito-m2m
django-cognito-m2m is a reusable Django library for machine-to-machine OAuth bearer-token authentication and authorization using AWS Cognito access tokens.
It integrates with the existing m2m-cognito Python library and treats that package as the source of truth for Cognito token validation. This package does not reimplement JWT verification. Instead, it provides Django and Django REST Framework integration around m2m_cognito.CognitoAccessTokenValidator.
The preferred long-term model is machine principal authentication:
request.authandrequest.service_principalare the canonical machine identity.request.usermapping exists to support staged migrations from legacy APIs that model clients as DjangoUserrows.- User mapping should be used carefully and intentionally.
Why This Exists
Teams often want Cognito-backed machine authentication in Django, but they need more than token validation:
- DRF authentication classes and reusable permissions
- Plain Django middleware, decorators, and CBV mixins
- Clear 401 vs 403 error behavior
- Consistent request access helpers
- Safe migration from user-backed API clients to service principals
This package provides those pieces while delegating Cognito token verification to m2m-cognito.
Relationship to m2m-cognito
m2m-cognito remains responsible for:
- fetching client-credentials tokens from Cognito
- validating Cognito JWT access tokens
- enforcing upstream token cryptography and Cognito-specific claim checks
django-cognito-m2m adds:
- Django and DRF request integration
- a canonical immutable service principal model
- scope and client-id authorization helpers
- optional Django user mapping and proxy-user compatibility
- API-friendly error responses
Installation
Install the base package and the upstream validator dependency:
pip install m2m-cognito django-cognito-m2m
If you want DRF support, install the extra:
pip install m2m-cognito 'django-cognito-m2m[drf]'
Supported Stack
- Python 3.10+
- Django 4.2 through 5.x
- Django REST Framework 3.15+ when using the
drfextra
Configuration
Add the settings block below to your Django project:
COGNITO_M2M = {
"REGION": "us-west-2",
"USER_POOL_ID": "us-west-2_AbCdEfGhI",
"VALIDATOR_CLASS": None,
"VALIDATOR_KWARGS": {},
"ALLOWED_CLIENT_IDS": None,
"AUDIENCE": None,
"HEADER_NAME": "HTTP_AUTHORIZATION",
"HEADER_PREFIX": "Bearer",
"REQUEST_PRINCIPAL_ATTR": "service_principal",
"REQUEST_AUTH_ATTR": "auth",
"DEFAULT_SCOPE_MATCH": "all",
"USER_MAPPING_ENABLED": False,
"USER_MAPPING_STRATEGY": None,
"USER_MAPPING_FIELD": None,
"USER_MAPPING_CLAIM": None,
"USER_MAPPING_CALLABLE": None,
"USER_MAPPING_CLASS": None,
"RETURN_USER_PROXY": False,
"FAIL_ON_INVALID_BEARER": True,
"JSON_ERROR_RESPONSES": True,
}
Important settings
REGIONandUSER_POOL_IDare required to construct the Cognito validator.VALIDATOR_CLASSlets you override the validator class with a dotted import path.VALIDATOR_KWARGSlets you pass additional constructor kwargs to the validator.HEADER_NAMEandHEADER_PREFIXcontrol bearer token extraction.ALLOWED_CLIENT_IDSacts as a default allowlist for permission and mixin layers.FAIL_ON_INVALID_BEARERcontrols whether plain Django middleware returns 401 immediately for invalid Bearer tokens.JSON_ERROR_RESPONSEScontrols whether plain Django errors return JSON or plain text.
Shared Principal Model
Every successful machine authentication resolves to django_cognito_m2m.principal.ServicePrincipal:
from django_cognito_m2m.principal import ServicePrincipal
principal.client_id
principal.scopes
principal.claims
principal.raw_token
principal.has_scope("widgets/read")
principal.has_scopes("widgets/read", "widgets/admin", match="any")
principal.sub
principal.aud
principal.iss
principal.exp
This principal is immutable and is the canonical machine identity across DRF and plain Django integrations.
DRF Quick Start
from rest_framework.response import Response
from rest_framework.views import APIView
from django_cognito_m2m.drf.authentication import CognitoM2MAuthentication
from django_cognito_m2m.drf.permissions import HasCognitoScopes
class WidgetListView(APIView):
authentication_classes = [CognitoM2MAuthentication]
permission_classes = [HasCognitoScopes]
required_scopes = {"widgets/read"}
def get(self, request):
principal = request.auth
return Response(
{
"client_id": principal.client_id,
"scopes": sorted(principal.scopes),
"user": getattr(request.user, "username", None),
}
)
DRF method-based scopes
from rest_framework.response import Response
from rest_framework.views import APIView
from django_cognito_m2m.drf.authentication import CognitoM2MAuthentication
from django_cognito_m2m.drf.permissions import MethodScopePermission
class WidgetView(APIView):
authentication_classes = [CognitoM2MAuthentication]
permission_classes = [MethodScopePermission]
scope_map = {
"GET": {"widgets/read"},
"POST": {"widgets/write"},
}
def get(self, request):
return Response({"ok": True})
def post(self, request):
return Response({"ok": True})
Available DRF permissions
HasCognitoScopesHasAllCognitoScopesHasAnyCognitoScopeMethodScopePermissionAllowedClientIdsPermission
DRF request contract
On successful machine authentication:
request.authis theServicePrincipalrequest.service_principalis also attached for consistencyrequest.useris one of:AnonymousUserby default- a mapped Django user when mapping is enabled
- a lightweight proxy user when
RETURN_USER_PROXY=True
Plain Django Quick Start
Middleware
Add the middleware when you want machine principals attached automatically:
MIDDLEWARE = [
# ...
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_cognito_m2m.django.middleware.CognitoM2MMiddleware",
]
The middleware is permissive by default:
- no Authorization header: request continues untouched
- non-Bearer scheme: request continues untouched
- valid Bearer token: principal is attached
- invalid Bearer token:
- returns 401 if
FAIL_ON_INVALID_BEARER=True - otherwise continues without an attached principal
- returns 401 if
Function views
from django.http import JsonResponse
from django_cognito_m2m.django.decorators import require_scopes
@require_scopes("widgets/read")
def widget_view(request):
principal = request.service_principal
return JsonResponse(
{
"client_id": principal.client_id,
"scopes": sorted(principal.scopes),
}
)
Class-based views
from django.http import JsonResponse
from django.views import View
from django_cognito_m2m.django.mixins import CognitoScopeRequiredMixin
class WidgetCBV(CognitoScopeRequiredMixin, View):
required_scopes = {"widgets/read"}
def get(self, request, *args, **kwargs):
return JsonResponse({"ok": True})
Available plain Django helpers
- decorators:
require_authenticationrequire_scopesrequire_any_scoperequire_all_scopesallow_client_ids
- CBV mixins:
CognitoAuthenticationRequiredMixinCognitoScopeRequiredMixinCognitoClientIdRequiredMixin
Authorization Patterns
Simple required scopes
required_scopes = {"widgets/read"}
Any/all matching
required_scopes = {"widgets/read", "widgets/admin"}
scope_match = "any"
Method-based scopes
scope_map = {
"GET": {"widgets/read"},
"POST": {"widgets/write"},
"PUT": {"widgets/write"},
"PATCH": {"widgets/write"},
"DELETE": {"widgets/write"},
}
Client allowlists
allowed_client_ids = {"my-reporting-client", "sync-worker"}
Staged Migration from Django User-Based API Auth
This package supports three practical modes.
Mode A: machine principal only
Default behavior:
request.authandrequest.service_principalcontain the machine principalrequest.userremainsAnonymousUser- no database lookup is required
This is the preferred long-term design.
Mode B: machine principal plus mapped Django user
Use this when older business logic or permission code still expects request.user:
COGNITO_M2M = {
"USER_MAPPING_ENABLED": True,
"USER_MAPPING_STRATEGY": "client_id_field",
"USER_MAPPING_FIELD": "username",
}
With this configuration, a validated principal whose client_id is reporting-client can map to User(username="reporting-client").
The machine identity still remains available on request.auth and request.service_principal.
Mode C: proxy user compatibility
Use this when you need a user-like object without requiring a database row:
COGNITO_M2M = {
"RETURN_USER_PROXY": True,
}
The proxy user:
- has
is_authenticated = True - has
is_anonymous = False - exposes
usernameandclient_id - keeps a back-reference to the
ServicePrincipal - does not pretend to be a real Django user row
User Mapping Strategies
Map by client_id to a user field
COGNITO_M2M = {
"USER_MAPPING_ENABLED": True,
"USER_MAPPING_STRATEGY": "client_id_field",
"USER_MAPPING_FIELD": "username",
}
Map by claim value to a user field
COGNITO_M2M = {
"USER_MAPPING_ENABLED": True,
"USER_MAPPING_STRATEGY": "claim_field",
"USER_MAPPING_FIELD": "username",
"USER_MAPPING_CLAIM": "sub",
}
Map with a callable
COGNITO_M2M = {
"USER_MAPPING_ENABLED": True,
"USER_MAPPING_STRATEGY": "callable",
"USER_MAPPING_CALLABLE": "my_project.auth.map_service_principal_to_user",
}
Map with a mapper class
COGNITO_M2M = {
"USER_MAPPING_ENABLED": True,
"USER_MAPPING_STRATEGY": "class",
"USER_MAPPING_CLASS": "my_project.auth.ServicePrincipalUserMapper",
}
Mapping safety guarantees
- user mapping is optional
- mapping misses do not authenticate as the wrong user
- the principal remains available even when a user is mapped
- ambiguous or failing lookups raise
UserMappingError
Request Principal Access Patterns
Application code should treat the principal as the source of machine identity:
principal = request.auth or request.service_principal
principal.client_id
principal.scopes
principal.has_scope("widgets/read")
You can also use the helper functions:
from django_cognito_m2m.utils import (
get_client_id,
get_scopes,
get_service_principal,
is_machine_authenticated,
)
Error Semantics
The package keeps authentication and authorization semantics explicit:
- missing token on protected endpoint:
401 - malformed Authorization header:
401 - invalid or expired token:
401 - valid token but missing scopes:
403 - valid token but client not allowed:
403
Default plain-Django JSON responses look like:
{"detail": "Authentication credentials were not provided."}
{"detail": "Invalid bearer token."}
{"detail": "Insufficient scope."}
Security and Design Notes
- Token validation is delegated to
m2m_cognito.CognitoAccessTokenValidator. - JWT/JWKS verification is not duplicated in this package.
- Authorization is explicit and endpoint-focused rather than hidden in global magic.
- Invalid bearer tokens are never silently treated as valid identities.
request.usercompatibility exists for migration, butrequest.authandrequest.service_principalremain canonical.
Testing
The project uses pytest and pytest-django.
Run the suite with:
pytest
The tests use a fake m2m_cognito-compatible validator so they do not depend on live Cognito, live JWTs, or network access to AWS.
The current test coverage includes:
- shared authenticator behavior
- principal normalization
- settings wiring and validator overrides
- DRF authentication and permissions
- method-based and action-based scopes
- middleware, decorators, and CBV mixins
- user mapping strategies and proxy-user behavior
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_cognito_m2m-0.1.1.tar.gz.
File metadata
- Download URL: django_cognito_m2m-0.1.1.tar.gz
- Upload date:
- Size: 54.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
231287a9841948d6d1c968f6f51247b513587394d96615403c4d0b1bbea960c9
|
|
| MD5 |
9ac5ed144d71699f575158cc2a7bd37a
|
|
| BLAKE2b-256 |
61b5a56fb1be0d6b3bf08de234e4a831e1ebd8e96520e17d42ee298b1f06d2f2
|
File details
Details for the file django_cognito_m2m-0.1.1-py3-none-any.whl.
File metadata
- Download URL: django_cognito_m2m-0.1.1-py3-none-any.whl
- Upload date:
- Size: 23.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
391587edd8f36ad896a43d31dae9b2e042197aa6eb539a0ee69bd1d7f3a2b63d
|
|
| MD5 |
89ef527d8e706c3b7176a068b38d5129
|
|
| BLAKE2b-256 |
3677000d9a370bfb8525b05b3d02d1cb70cb006eeb7e519900c9ecfcc3b404cd
|