Skip to main content

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.auth and request.service_principal are the canonical machine identity.
  • request.user mapping exists to support staged migrations from legacy APIs that model clients as Django User rows.
  • 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 drf extra

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

  • REGION and USER_POOL_ID are required to construct the Cognito validator.
  • VALIDATOR_CLASS lets you override the validator class with a dotted import path.
  • VALIDATOR_KWARGS lets you pass additional constructor kwargs to the validator.
  • HEADER_NAME and HEADER_PREFIX control bearer token extraction.
  • ALLOWED_CLIENT_IDS acts as a default allowlist for permission and mixin layers.
  • FAIL_ON_INVALID_BEARER controls whether plain Django middleware returns 401 immediately for invalid Bearer tokens.
  • JSON_ERROR_RESPONSES controls 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

  • HasCognitoScopes
  • HasAllCognitoScopes
  • HasAnyCognitoScope
  • MethodScopePermission
  • AllowedClientIdsPermission

DRF request contract

On successful machine authentication:

  • request.auth is the ServicePrincipal
  • request.service_principal is also attached for consistency
  • request.user is one of:
    • AnonymousUser by 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

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_authentication
    • require_scopes
    • require_any_scope
    • require_all_scopes
    • allow_client_ids
  • CBV mixins:
    • CognitoAuthenticationRequiredMixin
    • CognitoScopeRequiredMixin
    • CognitoClientIdRequiredMixin

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.auth and request.service_principal contain the machine principal
  • request.user remains AnonymousUser
  • 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 username and client_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.user compatibility exists for migration, but request.auth and request.service_principal remain 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_cognito_m2m-0.1.1.tar.gz (54.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_cognito_m2m-0.1.1-py3-none-any.whl (23.4 kB view details)

Uploaded Python 3

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

Hashes for django_cognito_m2m-0.1.1.tar.gz
Algorithm Hash digest
SHA256 231287a9841948d6d1c968f6f51247b513587394d96615403c4d0b1bbea960c9
MD5 9ac5ed144d71699f575158cc2a7bd37a
BLAKE2b-256 61b5a56fb1be0d6b3bf08de234e4a831e1ebd8e96520e17d42ee298b1f06d2f2

See more details on using hashes here.

File details

Details for the file django_cognito_m2m-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_cognito_m2m-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 391587edd8f36ad896a43d31dae9b2e042197aa6eb539a0ee69bd1d7f3a2b63d
MD5 89ef527d8e706c3b7176a068b38d5129
BLAKE2b-256 3677000d9a370bfb8525b05b3d02d1cb70cb006eeb7e519900c9ecfcc3b404cd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page