Auth¶
Backends, permissions, response builders, and the RFC 9728 PRM view.
Protocols¶
MCPAuthBackend ¶
Bases: Protocol
Pluggable authentication for the MCP transport.
The transport calls :meth:authenticate on every request. A backend that
returns None signals "no valid credentials" — the transport then emits
a 401 with WWW-Authenticate built from
:meth:www_authenticate_challenge. protected_resource_metadata powers
the /.well-known/oauth-protected-resource view (RFC 9728).
Backends MUST be safe to instantiate without arguments — settings-driven configuration belongs inside the backend's own module.
TokenInfo
dataclass
¶
Authenticated principal carried alongside an MCP request.
Backends construct this once per request and attach it to the Django
HttpRequest (as request.mcp_token). Permission classes consult it
to gate tool/resource access.
user: the resolved Django user (orAnonymousUser-equivalent).Anyhere because the user model is project-defined.scopes: OAuth scopes proven by the bearer token.audience: theaudclaim — must match the canonical/mcpURL per RFC 8707; backends are responsible for that comparison.raw: backend-specific opaque payload (theAccessTokenrow, the JWT claims dict, etc.) for advanced use cases.
MCPPermission ¶
Bases: Protocol
Per-tool / per-resource gate evaluated after authentication.
The transport pulls authenticated state from the request as a
:class:TokenInfo and asks each permission whether the call may proceed.
Returning False becomes a 403 + WWW-Authenticate; raising lets the
permission supply a richer payload via the JSON-RPC error path.
Permissions MUST be cheap to construct (we instantiate them per-binding at discovery time) and side-effect free at evaluation.
required_scopes ¶
Scopes to advertise in WWW-Authenticate when this permission denies.
Defaulting via Protocol: implementations that don't override this
should return []. We type it as a method (not a property) so it's
easy to compute dynamically from constructor args.
Backends¶
AllowAnyBackend ¶
Development / test backend that authenticates every request as anonymous.
DO NOT use in production. The protected_resource_metadata payload is
intentionally minimal so misconfiguration is loud rather than silent.
DjangoOAuthToolkitBackend ¶
Resource-server adapter for django-oauth-toolkit (DOT).
Validates the bearer token using DOT's own validators, then projects the
result into a :class:TokenInfo. The oauth2_provider package is
imported lazily inside authenticate because it's an optional extra
(pip install "djangorestframework-mcp-server[oauth]") — importing this module
never blows up just because DOT is absent; the ImportError only fires
when a request actually reaches authentication.
Audience enforcement (RFC 8707): when REST_FRAMEWORK_MCP["RESOURCE_URL"]
is configured, the token's resource claim must match exactly. Tokens
without a bound resource are rejected. Setting RESOURCE_URL to None
(the default) disables enforcement — appropriate for dev or for setups
where audience binding is performed by an upstream gateway.
Permissions¶
ScopeRequired ¶
Allow only requests whose token carries every listed OAuth scope.
Constructor takes the scopes positionally so usage stays compact:
ScopeRequired(["invoices:write"]).
DjangoPermRequired ¶
Allow only requests whose user has the given Django permission(s).
Wraps user.has_perm from the standard Django auth backend. A token
backed by AnonymousUser will always be rejected — that is the point
of using this class instead of :class:ScopeRequired.
Rate limits¶
MCPRateLimit ¶
Bases: Protocol
Per-binding rate limiter, evaluated after authentication and permissions.
The single consume call is the gate AND the bookkeeping update — there
is no separate "check then commit" because that pattern races under
concurrency. Implementations decrement quotas atomically in storage; the
return value is the suggested Retry-After in seconds when the limit
has been hit, or None to allow the call.
Returning 0 is allowed (e.g. "denied but window resets immediately")
but most implementations will return a positive integer.
Limiters are constructed per-binding at registration time; keep them cheap to construct and thread-safe at evaluation. State that crosses requests must live in shared storage (Django cache, Redis, …), not on the instance — instance state would be lost the moment the binding is re-used across worker processes.
FixedWindowRateLimit ¶
A fixed-window-counter rate limiter backed by django.core.cache.
The window is bucketed by absolute time: each integer multiple of
per_seconds since the epoch starts a new counter. Simple and
sufficient for protecting against runaway clients; not as smooth as a
sliding window but doesn't require a sorted set.
The cache key is namespaced with namespace so multiple limits on the
same binding (e.g. burst + steady-state) don't share counters. key
customises the bucket dimension — defaults to per-token-user.
The cache must be a shared backend in multi-process deployments;
Django's locmem cache is fine for tests but won't enforce a global
limit across worker processes.
SlidingWindowRateLimit ¶
Sliding-window rate limiter using a list of timestamps in cache.
Avoids the well-known fixed-window edge case where a client can issue
2 * max_calls requests across two adjacent windows. Stores the
timestamps of recent calls in a Django cache entry; on each call,
expired entries are pruned and the live count is compared against
max_calls.
Trade-offs vs :class:FixedWindowRateLimit:
- Smoother: limits actual request rate over the trailing
per_secondswindow, not bucketed counts. - Memory cost: the timestamp list grows with traffic up to
max_callsentries per key; negligible for typical limits. - Read-modify-write: doesn't have the atomic guarantees of the
fixed-window's
cache.add+cache.incrprimitives. Concurrent calls can see slightly stale state and admit a small number of extra requests under contention. For strict atomicity in multi-worker deployments, use a Redis-backed limiter with Lua.
The cache must be a shared backend in multi-process deployments;
Django's locmem works for tests but won't share state across
workers.
TokenBucketRateLimit ¶
Token-bucket rate limiter using Django cache for state.
A bucket has at most capacity tokens. Each accepted call consumes
one token; the bucket refills continuously at refill_per_second
tokens per second. When empty, the limiter returns the suggested
retry-after time until at least one token is available again.
Trade-offs vs the sliding-window scheme:
- Burst-friendly: a full bucket can absorb a burst of
capacityrequests instantly, then rate-limit the steady state atrefill_per_second. Useful when consumers naturally batch. - Read-modify-write: like the sliding-window class, this is not strictly atomic across concurrent workers. Under contention a small number of extra tokens may slip through. For strict atomicity in multi-worker deployments, back the limiter with a Redis-Lua script.
The cache must be a shared backend (Memcached or Redis) in
multi-process deployments; Django's locmem is fine for tests but
won't share state across workers.
Response helpers¶
build_unauthenticated_response ¶
Build a spec-compliant 401 response with the supplied WWW-Authenticate value.
The body is a small JSON envelope so MCP clients that surface error payloads to the user see a meaningful message rather than an empty body.
build_insufficient_scope_response ¶
Build a 403 response signalling missing OAuth scope.
Per RFC 6750, the error="insufficient_scope" value belongs in the
WWW-Authenticate header — that's already baked into challenge.
ProtectedResourceMetadataView ¶
Bases: View
RFC 9728 OAuth 2.0 Protected Resource Metadata endpoint.
Mounted at /.well-known/oauth-protected-resource by :class:MCPServer.
Delegates payload construction to the configured
:class:MCPAuthBackend.protected_resource_metadata. The backend is
instance-scoped — passed in via as_view — so multiple servers in one
process can advertise different metadata.