Skip to content

Auth

Backends, permissions, response builders, rate limits, and OAuth-related views (RFC 9728 PRM + the opt-in contrib oauth/ mount).

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) and returns a :class:ProtectedResourceMetadata dataclass; the contrib PRM ViewSet calls .to_dict() for the wire shape.

:meth:authorization_server_metadata is consumed by the optional rest_framework_mcp.contrib.oauth mount (Phase 10d+) — backends that host an authorization server return an :class:AuthorizationServerMetadata dataclass; backends that don't host one raise :class:NotImplementedError so the contrib code can skip the AS endpoint matrix cleanly.

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 (or AnonymousUser-equivalent). Any here because the user model is project-defined.
  • scopes: OAuth scopes proven by the bearer token.
  • audience: the aud claim — must match the canonical /mcp URL per RFC 8707; backends are responsible for that comparison.
  • raw: backend-specific opaque payload (the AccessToken row, 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

required_scopes() -> list[str]

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.

authorization_server_metadata

authorization_server_metadata() -> AuthorizationServerMetadata

Return the RFC 8414 metadata payload for the DOT-hosted authorization server.

Pulls the issuer, endpoint URLs, supported grant / response types, and scopes from :setting:REST_FRAMEWORK_MCP['SERVER_INFO']'s authorization_servers key (first entry) plus the contrib.oauth mount convention that endpoints live at /oauth/authorize/, /oauth/token/, /oauth/register/.

Missing values fall through as empty strings / lists so the wire shape is always valid JSON; consumers are expected to populate SERVER_INFO for production deployments.

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.

DRFPermissionAdapter

Bridge a DRF BasePermission class into the :class:MCPPermission Protocol.

Sister-repo 0.12.0 added permission_classes on ServiceSpec and SelectorSpec — a sequence of DRF BasePermission classes. The MCP transport doesn't go through DRF views, so each class is wrapped in this adapter at registration time; the wrapped instance is constructed once (mirrors sister-repo's [perm() for perm in spec.permission_classes] in its view get_permissions).

The DRF instance receives a synthesised :class:rest_framework.request.Request with user set to token.user and a lightweight view stand-in sufficient for the DRF permission contract (request, action). The HTTP method on the underlying HttpRequest is left untouched (unlike :func:build_internal_drf_request, which forces POST for mutation-flow dispatch) — permission evaluation is method-agnostic.

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_seconds window, not bucketed counts.
  • Memory cost: the timestamp list grows with traffic up to max_calls entries per key; negligible for typical limits.
  • Read-modify-write: doesn't have the atomic guarantees of the fixed-window's cache.add + cache.incr primitives. 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 capacity requests instantly, then rate-limit the steady state at refill_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_unauthenticated_response(challenge: str) -> HttpResponse

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_insufficient_scope_response(challenge: str) -> HttpResponse

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.

Protected Resource Metadata (RFC 9728)

ProtectedResourceMetadataViewSet

Bases: ViewSet

RFC 9728 OAuth 2.0 Protected Resource Metadata endpoint.

Mounted at /.well-known/oauth-protected-resource by :class:MCPServer. Single-action ViewSet — the canonical GET is wired as list via ProtectedResourceMetadataViewSet.as_view({"get": "list"}, auth_backend=...) so the URL conf doesn't need a router.

Delegates payload construction to the configured :meth:MCPAuthBackend.protected_resource_metadata, which returns a :class:ProtectedResourceMetadata dataclass. The backend is instance-scoped — passed in via as_view — so multiple servers in one process can advertise different metadata.

DRF's default authentication / permission / throttling layers are disabled (empty lists below): PRM is a public discovery endpoint by design and the MCP transport owns its own auth pipeline through :class:MCPAuthBackend. The renderer is pinned to JSON because the payload shape is RFC-defined; content negotiation would be noise.

ProtectedResourceMetadata dataclass

RFC 9728 OAuth 2.0 Protected Resource Metadata payload.

Returned by :meth:MCPAuthBackend.protected_resource_metadata and serialised onto the wire by the PRM ViewSet. Keys map 1:1 to the RFC 9728 field names; warning is the package-local extension that AllowAnyBackend uses to make dev-mode misconfiguration loud.

OAuth contrib (opt-in)

build_oauth_urlpatterns(server, *, include_dcr=False, include_aliases=True, include_openid_discovery=True) returns a list of URL patterns ready to mount alongside your MCPServer.urls. Exposes RFC 8414 / OIDC discovery / RFC 7591 Dynamic Client Registration + the alias paths different LLM hosts probe (aliases render the canonical payload — they are not HTTP redirects).

build_oauth_urlpatterns

build_oauth_urlpatterns(
    *,
    server: MCPServer,
    include_dcr: bool = False,
    include_aliases: bool = True,
    include_openid_discovery: bool = True,
    include_authorize: bool = False,
) -> list[URLPattern]

Return URL patterns for the OAuth endpoint matrix.

Endpoint matrix when all flags are True:

+---------------------------------------------------------+--------------------------------------+ | Path | View | +=========================================================+======================================+ | /.well-known/oauth-protected-resource | ProtectedResourceMetadataViewSet | +---------------------------------------------------------+--------------------------------------+ | /.well-known/oauth-protected-resource/mcp | alias | +---------------------------------------------------------+--------------------------------------+ | /mcp/.well-known/oauth-protected-resource | alias | +---------------------------------------------------------+--------------------------------------+ | /.well-known/oauth-authorization-server | AuthorizationServerMetadataViewSet | +---------------------------------------------------------+--------------------------------------+ | /.well-known/oauth-authorization-server/oauth | alias | +---------------------------------------------------------+--------------------------------------+ | /oauth/.well-known/oauth-authorization-server | alias | +---------------------------------------------------------+--------------------------------------+ | /.well-known/openid-configuration | OpenIDDiscoveryViewSet | +---------------------------------------------------------+--------------------------------------+ | /.well-known/openid-configuration/oauth | alias | +---------------------------------------------------------+--------------------------------------+ | /oauth/register/ | DynamicClientRegistrationViewSet | +---------------------------------------------------------+--------------------------------------+

The DOT-provided /oauth/authorize/ and /oauth/token/ views are NOT mounted here — consumers include oauth2_provider.urls separately. The contrib mount focuses on the discovery / DCR surface; the AS endpoints themselves belong to whichever framework actually hosts the AS.

Parameters:

Name Type Description Default
server MCPServer

The :class:MCPServer whose auth_backend should drive all of the discovery payloads. Passed in instead of looked up from settings so multi-server deployments work.

required
include_dcr bool

Mount /oauth/register/. Default False because DCR is gated behind REST_FRAMEWORK_MCP['DCR_ENABLED'] anyway and consumers who don't need it shouldn't even see the URL.

False
include_aliases bool

Mount the alias URLs alongside the canonical ones. Default True because clients in the wild use varied paths.

True
include_openid_discovery bool

Mount the OIDC discovery alias. Default True for the same reason.

True
include_authorize bool

Mount /oauth/authorize/ as a thin DOT :class:AuthorizationView subclass with the configured :class:AuthUserAdapter hook. Default False because the consumer's own URL conf typically owns /oauth/authorize/ via include('oauth2_provider.urls'); flip this on when you want the adapter wired and you're not including DOT's urls otherwise. Requires the [oauth] extra — DOT is imported lazily inside the factory.

False

AuthorizationServerMetadataViewSet

Bases: ViewSet

RFC 8414 OAuth 2.0 Authorization Server Metadata endpoint.

Mounted by :func:build_oauth_urlpatterns at the canonical /.well-known/oauth-authorization-server URL plus aliases. The canonical GET wires up as the list action via AuthorizationServerMetadataViewSet.as_view({"get": "list"}, auth_backend=...).

Delegates payload construction to :meth:MCPAuthBackend.authorization_server_metadata, which returns an :class:AuthorizationServerMetadata dataclass. Backends that don't host an authorization server raise :class:NotImplementedError on the method; this view surfaces that as 501 Not Implemented so clients see a deterministic "no AS here" signal rather than a 500.

DRF's default auth / permission / throttling layers are disabled — discovery is public and the MCP transport owns its own pipeline. The renderer is pinned to JSON; the payload shape is RFC-defined.

OpenIDDiscoveryViewSet

Bases: ViewSet

OIDC discovery alias — /.well-known/openid-configuration.

Some MCP / LLM-host clients probe /.well-known/openid-configuration before falling back to RFC 8414. This ViewSet returns the same payload as :class:AuthorizationServerMetadataViewSet plus a small set of OIDC defaults so the probe succeeds even though :mod:rest_framework_mcp doesn't implement an actual ID-token endpoint.

Single-action ViewSet — the canonical GET wires up as list via OpenIDDiscoveryViewSet.as_view({"get": "list"}, auth_backend=...).

Concretely, the payload is the backend's AS metadata composed with:

  • subject_types_supported: ["public"] — DOT-style pseudonymous identifiers.
  • id_token_signing_alg_values_supported: ["RS256"] — the most common OIDC algorithm, advertised even though we don't actually mint ID tokens (clients that walk this list and pick one work fine because they only use it for verification — which they never get to do without an ID-token endpoint anyway).
  • response_modes_supported: ["query"] — standard.

Backends that don't host an authorization server raise :class:NotImplementedError; this view surfaces that as 501 for parity with :class:AuthorizationServerMetadataViewSet.

DynamicClientRegistrationViewSet

Bases: ViewSet

RFC 7591 Dynamic Client Registration endpoint.

Default state is locked down: DCR_ENABLED=False produces a 403 on every request. To turn DCR on, set the flag in REST_FRAMEWORK_MCP settings and (recommended) also set DCR_INITIAL_ACCESS_TOKEN to a static bearer that clients must present.

Single-action ViewSet — wired as the create action (POST) via DynamicClientRegistrationViewSet.as_view({"post": "create"}). Successful POST returns the RFC 7591 client information response (client_id / client_secret / echoed registration metadata) and persists a DOT Application.

DOT (oauth2_provider) is imported lazily inside the action so this module remains importable without the [oauth] extra. A request that arrives with DCR enabled but DOT absent surfaces a clear ImportError at first use rather than at server startup.

DRF's default auth / permission / throttling layers are disabled — DCR is gated by the dedicated DCR_INITIAL_ACCESS_TOKEN setting, not by DRF's session/token authenticators. The CSRF / session middleware is sidestepped because DRF's APIView.dispatch (which ViewSet inherits) wraps responses with csrf_exempt semantics when no SessionAuthentication class is configured.

DynamicClientRegistrationSerializer

Bases: DataclassSerializer

RFC 7591 dynamic client registration request shape.

Wraps :class:DynamicClientRegistrationRequest so the validated payload arrives at :class:DynamicClientRegistrationViewSet as a typed dataclass instance (via .save()). Field overrides below replace the dataclass-derived auto-generated fields with shapes that actually validate the wire contract:

  • redirect_uris is required + non-empty + child is a URL.
  • client_type / authorization_grant_type are ChoiceField with choices sourced from DOT's Application model constants at instance construction. Declaring them here lets us reject malformed values with a per-field error before the request ever reaches the database. The lazy import keeps this module importable without the [oauth] extra.

Other RFC 7591 fields are silently ignored — DOT doesn't model them and inventing a richer shape would diverge from the underlying authorization server.

AuthorizationServerMetadata dataclass

RFC 8414 OAuth 2.0 Authorization Server Metadata payload.

Returned by :meth:MCPAuthBackend.authorization_server_metadata and serialised by the contrib AS metadata ViewSet. Backends that don't host an AS raise :class:NotImplementedError instead of returning this dataclass; the calling ViewSet maps the exception to 501 Not Implemented.

Field shapes mirror RFC 8414; str-typed endpoints default to "" so the wire shape is always valid JSON even when the configuration is incomplete (callers can populate SERVER_INFO to fill them).

OpenIDDiscoveryPayload dataclass

OIDC discovery alias payload — extends :class:AuthorizationServerMetadata.

Composes (not subclasses) the AS metadata so the underlying type stays exactly RFC 8414. The OIDC additions (subject_types_supported, id_token_signing_alg_values_supported, response_modes_supported) are advertised because some MCP / LLM-host clients probe /.well-known/openid-configuration first and skip the probe silently if these keys are absent.

See :class:OpenIDDiscoveryViewSet for the rationale on returning OIDC-shaped metadata without implementing an actual ID-token endpoint.

DynamicClientRegistrationRequest dataclass

RFC 7591 dynamic client registration request payload.

Mutable dataclass so :class:DynamicClientRegistrationSerializer (a DataclassSerializer over this type) can apply defaults via setdefault-style normalisation if needed. Frozen would force consumers to build a second instance just to change a defaulted field, which is awkward for a request-shape type.

Whitelists the fields the DCR view actually forwards to DOT's Application model. Other RFC 7591 fields are silently ignored by the serializer — DOT doesn't model them.

DynamicClientRegistrationResponse dataclass

RFC 7591 client information response.

The wire shape returned by :class:DynamicClientRegistrationViewSet on a successful registration. scope is optional (only emitted when the request supplied one).

DCR is gated behind two settings:

  • REST_FRAMEWORK_MCP["DCR_ENABLED"] (default False) — DCR endpoint returns 501 Not Implemented while disabled.
  • REST_FRAMEWORK_MCP["DCR_INITIAL_ACCESS_TOKEN"] (default None) — optional bearer required on the DCR POST, per RFC 7591 §3.

AuthUserAdapter

Bases: Protocol

Hydrate request.user before DOT's AuthorizationView dispatches.

The common production setup is "DRF backend with SimpleJWT cookies on the same host". DOT's AuthorizationView only knows about Django's standard session-based request.user — without an adapter, a JWT-authenticated user appears anonymous to the OAuth flow and the consent screen gets shown again. The adapter is the seam where the consumer's preferred authentication scheme decides which user the OAuth flow should attribute the grant to.

Implementations return:

  • The authenticated :class:AbstractBaseUser to set on the request before delegating to DOT.
  • None to leave request.user untouched — DOT then falls back to its own session-based flow (which may redirect to login).

Adapters MUST be safe to instantiate without arguments — settings-driven configuration belongs inside the adapter's own module so the dotted-path setting can resolve it directly.

SimpleJWTCookieAdapter

Reference :class:AuthUserAdapter for SimpleJWT cookie-authenticated apps.

Reads the access-token cookie (name configured via REST_FRAMEWORK_MCP['SIMPLEJWT_ACCESS_COOKIE'], default "access"), decodes it with :class:rest_framework_simplejwt.tokens.AccessToken, looks the user up by primary key, and returns it. Returns None for any failure mode (no cookie, malformed token, expired token, unknown user) — DOT's view then falls back to its session-based flow.

rest_framework_simplejwt is imported lazily inside :meth:hydrate so this module remains importable without the [jwt] extra. A consumer who configures this adapter without the extra installed surfaces a clear ImportError at first request, not at import.