Django REST Framework API key authentication¶
Authenticating requests¶
# myapp/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from django_tenant_apikeys.authentication import TenantAPIKeyAuthentication
from django_tenant_apikeys.permissions import HasAPIKeyScope
class DeploymentsView(APIView):
authentication_classes = [TenantAPIKeyAuthentication]
permission_classes = [HasAPIKeyScope]
required_scopes = ["deployments:write"]
def post(self, request):
# request.auth is the authenticated OrganizationAPIKey instance
# request.tenant is request.auth.tenant, attached automatically
request.tenant.deployments.create(...)
return Response(status=201)
Or wire it globally instead of per view:
# settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"django_tenant_apikeys.authentication.TenantAPIKeyAuthentication",
],
}
Clients send:
A few things worth knowing about authenticate():
- No
Authorizationheader, or a scheme other thanApi-Key? It returnsNoneand gets out of the way, so any other authenticator you have configured gets a turn. - Scheme matches but the key is missing, malformed, unrecognized, tampered
with, deactivated, or expired? It raises
AuthenticationFailed— a plain 401, same as DRF's built-in authenticators. See Authentication for the exact check order and messages. - On success it returns
(None, api_key_instance). TheNonewhere a DjangoUserwould normally sit is intentional — an API key authenticates an integration, not a person, sorequest.userstays anonymous andrequest.authis where the key instance actually lives.
Running more than one kind of key in the same project? Subclass instead of juggling settings:
Scope checks¶
class OrdersView(APIView):
authentication_classes = [TenantAPIKeyAuthentication]
permission_classes = [HasAPIKeyScope]
required_scopes = ["orders:read", "orders:write"] # all of these, not any
Full detail, including per-action scopes on a ViewSet: Scopes.
IP restrictions¶
from django_tenant_apikeys.permissions import HasAllowedIP
class OrdersView(APIView):
authentication_classes = [TenantAPIKeyAuthentication]
permission_classes = [HasAllowedIP, HasAPIKeyScope]
required_scopes = ["orders:read"]
Full detail, including trusted-proxy-header configuration: IP restrictions.
Rate limiting¶
from django_tenant_apikeys.permissions import WithinRateLimit
class OrdersView(APIView):
authentication_classes = [TenantAPIKeyAuthentication]
permission_classes = [WithinRateLimit, HasAPIKeyScope]
required_scopes = ["orders:read"]
Full detail, including how the default backend counts requests and its multi-process limitations: Rate limiting.
How the policies interact¶
Combine all three (or any subset) in permission_classes, in this order:
DRF checks permission_classes in list order, and stops at the first one
that denies the request — so the order you list them in is the order they
actually run in:
Authentication (extract, verify, active/expired, resolve tenant)
↓
IP restriction (HasAllowedIP)
↓
Rate limit (WithinRateLimit)
↓
Scope (HasAPIKeyScope)
↓
Your view
A request from a disallowed IP never reaches the rate limiter or the scope check. A request over its rate limit never reaches the scope check, but does still count against — and gets rejected by — the rate limiter even if it would've failed the scope check anyway, since the rate limiter runs first.
TenantAPIKeyAuthentication itself hasn't changed shape for any of this —
it still only extracts the key, verifies it, checks
is_active/expires_at, and resolves request.tenant. IP, rate limit, and
scope are separate, independently testable permission classes layered on
top, not folded into authentication. None of them are required — a
permission_classes = [HasAPIKeyScope] view from before these existed
still behaves exactly as it did.
Next¶
- Django Ninja — the same policies, without DRF's permission-class system.
- Multi-tenancy — how
request.tenantgets resolved. - Testing — testing a DRF authentication/permission chain without spinning up real URL routing.