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 authenticate on every request; returning
None signals "no valid credentials" and the transport emits a 401 whose
WWW-Authenticate comes from www_authenticate_challenge.
protected_resource_metadata powers the
/.well-known/oauth-protected-resource view (RFC 9728); the PRM ViewSet
calls .to_dict() on the returned dataclass for the wire shape.
authorization_server_metadata is consumed by the optional
rest_framework_mcp.contrib.oauth mount. A backend that hosts no
authorization server raises 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.
Attributes:
| Name | Type | Description |
|---|---|---|
user |
Any
|
The resolved Django user, or an |
scopes |
tuple[str, ...]
|
OAuth scopes proven by the bearer token. |
audience |
str | None
|
The |
raw |
Any
|
Backend-specific opaque payload — the |
MCPPermission ¶
Bases: Protocol
Per-tool / per-resource gate evaluated after authentication.
The transport pulls authenticated state from the request as a
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 — they are instantiated per binding at discovery time — and side-effect free at evaluation.
required_scopes ¶
Scopes to advertise in WWW-Authenticate when this permission denies.
Implementations with no scope semantics return [].
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 TokenInfo.
oauth2_provider is imported lazily inside the method bodies because it is an
optional extra (pip install "djangorestframework-mcp-server[oauth]"): importing
this module without DOT is fine, and the ImportError fires only when a request
actually reaches authentication.
Audience enforcement (RFC 8707) is controlled by enforce_audience,
not by resource_url. resource_url is the identity this server
publishes in its protected-resource metadata; enforcement is whether a
token that doesn't carry that identity is rejected. resource_url is
effectively required by RFC 9728; enforcement is a separate decision.
The [oauth] extra floors django-oauth-toolkit at >=3.4, so this
works out of the box. That release added RFC 8707 resource indicators:
stock AccessToken carries a resource field and an
allows_audience check, so turning enforcement on needs nothing else.
The default nonetheless stays off, and the reason is no longer the floor.
audience_matches rejects a token carrying no resource, and a token
only carries one if the client sent the RFC 8707 resource parameter at
the authorize and token endpoints. The MCP specification requires clients
to send it, but the authorization server this backend reads is a general
OAuth server whose other clients have no such obligation — so defaulting
True would 401 every token minted for a browser app, a script, or an
MCP client that has not caught up, on the release that changed the default.
UnenforcedAudienceWarning is how a deployment that could conform is
told so, at construction, instead.
With a swapped model that drops the field, or a DOT installed outside this extra and pinned below 3.4, enforcement still works if the resource is somewhere else:
- a swapped
OAUTH2_PROVIDER["ACCESS_TOKEN_MODEL"]carrying aresourcefield (DOT supports substituting the model), or - an explicit
audience_getter=reading it from wherever it lives — a JWT claim, an upstream gateway header, a related row.
The check is capability-based rather than version-based: it asks the
configured token model whether it has the field. Turning enforcement on with
no route to the resource raises ImproperlyConfigured at construction,
naming every way out, instead of 401-ing every request.
One resource URL per server. RFC 8707 binds a token to a resource,
and that binding is precisely what stops a token issued for one resource
being replayed against another: two servers sharing a single URL would let
a token minted for /public/mcp satisfy /internal/mcp. Hence
resource_url is per-backend and the RESOURCE_URL setting is only
its default:
MCPServer(
name="internal",
resource_url="https://example.com/internal/mcp/",
)
Every value is resolved once, here — the settings reads are argument defaults, not per-request lookups — so two backends in one process can genuinely differ.
check_configuration ¶
Refuse at mount time if DOT cannot serve a request, rather than at the first one.
Implements the opt-in
:class:~rest_framework_mcp.auth.types.self_checking.SelfChecking
protocol. The import stays lazy everywhere else on purpose -- see the
class docstring -- and this does not change that: an in-process server
is never mounted, so it is never called.
Two things have to be true, and they fail differently. The
distribution has to be installed, and the app has to be in
INSTALLED_APPS, because authenticate resolves DOT's access-token
model and a model needs the app registry rather than the module. The
first version of this checked only the import and let the second case
through -- a project that had installed the extra got past the mount and
then failed on the first request with a RuntimeError about
app_label, naming neither this package nor the setting that fixes
it.
The order is the order the two fail in. apps.is_installed is False
both when the app is missing from the setting and when the distribution
is absent entirely, so asking it first would tell someone without the
extra to edit a setting.
authorization_server_metadata ¶
Return the RFC 8414 metadata payload for the DOT-hosted authorization server.
Endpoints are built from this backend's first authorization_servers
entry plus the endpoint paths, which default to the contrib.oauth
mount convention (/oauth/authorize/, /oauth/token/,
/oauth/register/) and are constructor arguments for a project that
mounts DOT somewhere else.
The issuer is treated as a site root and the paths are appended to
it, so passing the value DOT advertises as its issuer
(<host>/oauth) publishes <host>/oauth/oauth/authorize/ — three
404s in a document clients read to find the login flow. That mistake
warns at construction; see MountedAuthorizationServerWarning.
Missing values fall through as empty strings / lists so the wire shape
is always valid JSON; configure authorization_servers for
production. client_id_metadata_document_supported is read from DOT
rather than asserted here — see _cimd_enabled.
registration_endpoint is advertised whenever an issuer is
configured, including where DCR is switched off, and that is
deliberate on two counts.
Structurally it is the only answer available here. Whether the endpoint
accepts registrations is decided per mount:
build_oauth_urlpatterns(dcr_enabled=...) resolves it into
as_view(...), and REST_FRAMEWORK_MCP['DCR_ENABLED'] is only that
argument's default. This backend never sees the argument, so gating the
advertisement on the global would withdraw a working endpoint from any
deployment that enables DCR at the mount — the flow that exists so two
mounts in one project can differ. A client that registers against a
disabled endpoint gets an immediate, well-formed 403
invalid_request, not a hang.
By protocol the advertisement also costs a modern client nothing. The
2026-07-28 registration priority order is pre-registration, then
Client ID Metadata Documents, then DCR, so a client that can read
client_id_metadata_document_supported never reaches
registration_endpoint. It is read only by clients with no other way
in, which is still most of them.
Permissions¶
ScopeRequired ¶
Allow only requests whose token carries every listed OAuth scope.
Takes a list, or a bare string for the single-scope case:
ScopeRequired(["invoices:read", "invoices:write"])
ScopeRequired("invoices:write")
The bare string is not sugar — it closes a trap. Normalising with
list(scopes) would silently turn ScopeRequired("mcp:admin") into nine
one-character scopes: nothing fails at registration, and the misconfiguration
surfaces much later as a permission that can never be satisfied and a nonsense
challenge.
DjangoPermRequired
takes a bare string too, so the siblings agree.
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 is always rejected — that is the point of using this class instead
of
ScopeRequired.
Takes a list or a bare string, like
ScopeRequired.
DRFPermissionAdapter ¶
Bridge a DRF BasePermission class into the
MCPPermission
Protocol.
ServiceSpec / SelectorSpec carry permission_classes as DRF
BasePermission classes, and the MCP transport doesn't go through DRF
views, so each class is wrapped here at registration time and instantiated
once — mirroring what a DRF view's get_permissions does.
The DRF instance receives a synthesised 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
build_offline_context, which forces
POST for mutation dispatch — because permission evaluation is
method-agnostic.
auth is set alongside user, and has to be. DRF resolves
request.auth lazily: reading it on a request that has never
authenticated runs the (here empty) authenticator chain, which ends in
_not_authenticated() and overwrites request.user with
UNAUTHENTICATED_USER — on the wrapper and on the HttpRequest
underneath it. A permission class as ordinary as
TokenHasScope reads request.auth first and every request.user
read after it would see AnonymousUser, denying a properly scoped caller
with nothing in the response explaining why. Assigning the backend's opaque
payload — DRF's own convention for what auth holds — means the getter
never reaches for the chain.
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 and
return the suggested Retry-After in seconds once the limit is hit
(0 is legal, meaning the window resets immediately), or None to
allow the call.
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, which is not shared 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. Not as smooth as a
sliding window, but it needs no sorted set.
namespace keeps multiple limits on one binding (burst plus
steady-state) from sharing counters; key customises the bucket
dimension and defaults to per-token-user.
The cache must be a shared backend in multi-process deployments;
Django's locmem is fine for tests but enforces no global limit across
worker processes.
SlidingWindowRateLimit ¶
Sliding-window rate limiter using a list of timestamps in cache.
Avoids the fixed-window edge case where a client issues 2 * max_calls
requests across two adjacent windows: the timestamps of recent calls are
stored in a cache entry, and each call prunes the expired ones and compares
the live count against max_calls.
Trade-offs against
FixedWindowRateLimit:
- Smoother: limits the actual rate over the trailing
per_seconds, not bucketed counts. - Memory cost: up to
max_callstimestamps per key. - Read-modify-write: no atomic guarantee, unlike the fixed window's
cache.add+cache.incr. Concurrent calls can read stale state and admit a few extra requests under contention; for strict atomicity in a multi-worker deployment, back the limiter with a Redis-Lua script.
The cache must be a shared backend in multi-process deployments;
Django's locmem works for tests but shares no state across workers.
TokenBucketRateLimit ¶
Token-bucket rate limiter using Django cache for state.
A bucket holds at most capacity tokens, each accepted call consumes
one, and the bucket refills continuously at refill_per_second. When
empty, the limiter returns the time until one token is available again.
Trade-offs against the sliding-window scheme:
- Burst-friendly: a full bucket absorbs
capacityrequests instantly, then holds the steady state atrefill_per_second. Useful when consumers naturally batch. - Read-modify-write: like the sliding window, not strictly atomic across workers, so a few extra tokens can slip through under contention. For strict atomicity, 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 shares
no 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.
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
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. The payload comes from the instance-scoped
MCPAuthBackend.protected_resource_metadata, so multiple servers in one process
advertise different metadata.
DRF authentication and permissions are deliberately open: PRM is a public discovery
endpoint, and the MCP transport owns its own auth pipeline through
MCPAuthBackend. The
renderer is pinned to JSON because the payload shape is RFC-defined.
ProtectedResourceMetadata
dataclass
¶
RFC 9728 OAuth 2.0 Protected Resource Metadata payload.
Returned by MCPAuthBackend.protected_resource_metadata and
serialised onto the wire by the PRM ViewSet. Keys map 1:1 to the RFC 9728
field names, except warning: a package-local extension serialised as
_warning, which AllowAnyBackend uses to make dev-mode
misconfiguration loud in client tooling.
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,
auth_user_adapter: AuthUserAdapter | None = None,
dcr_enabled: bool | None = None,
dcr_initial_access_token: str | None = None,
) -> list[URLPattern]
Return URL patterns for the OAuth endpoint matrix.
With every flag on, the canonical paths and their aliases are:
/.well-known/oauth-protected-resource ProtectedResourceMetadataViewSet
+ /.well-known/oauth-protected-resource/mcp
+ /mcp/.well-known/oauth-protected-resource
/.well-known/oauth-authorization-server AuthorizationServerMetadataViewSet
+ /.well-known/oauth-authorization-server/oauth
+ /oauth/.well-known/oauth-authorization-server
/.well-known/openid-configuration OpenIDDiscoveryViewSet
+ /.well-known/openid-configuration/oauth
/oauth/register/ DynamicClientRegistrationViewSet
DOT's own /oauth/authorize/ and /oauth/token/ are not mounted:
this covers the discovery and DCR surface, while the AS endpoints belong to
whichever framework hosts the authorization server. Every argument is
resolved here, when the patterns are built, rather than per request, so two
mounts in one project can differ.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
MCPServer
|
The |
required |
include_dcr
|
bool
|
Mount |
False
|
include_aliases
|
bool
|
Mount the alias URLs alongside the canonical ones. |
True
|
include_openid_discovery
|
bool
|
Mount the OIDC discovery alias. |
True
|
include_authorize
|
bool
|
Mount |
False
|
auth_user_adapter
|
AuthUserAdapter | None
|
Hydrates |
None
|
dcr_enabled
|
bool | None
|
Whether |
None
|
dcr_initial_access_token
|
str | None
|
RFC 7591 §3 token a DCR client must present.
|
None
|
AuthorizationServerMetadataViewSet ¶
Bases: ViewSet
RFC 8414 OAuth 2.0 Authorization Server Metadata endpoint.
Mounted by
build_oauth_urlpatterns
at /.well-known/oauth-authorization-server plus aliases, wired as the list
action: as_view({"get": "list"}, auth_backend=...).
The payload comes from
MCPAuthBackend.authorization_server_metadata. A backend that hosts
no authorization server raises NotImplementedError, which this
view surfaces as 501 so clients get a deterministic "no AS here" rather
than a 500.
DRF's default auth / permission / throttling layers are off: discovery is public and the MCP transport owns its own pipeline. The renderer is pinned to JSON, the payload shape being RFC-defined.
OpenIDDiscoveryViewSet ¶
Bases: ViewSet
OIDC discovery alias — /.well-known/openid-configuration.
Some MCP hosts probe this path before falling back to RFC 8414, so the
payload is the backend's AS metadata plus a few OIDC defaults, letting the
probe succeed even though this package implements no ID-token endpoint.
Wired as the list action:
as_view({"get": "list"}, auth_backend=...).
The additions are subject_types_supported: ["public"],
response_modes_supported: ["query"], and
id_token_signing_alg_values_supported — the last derived rather
than fixed, because wherever DOT is the authorization server with
OIDC_ENABLED its token endpoint really does mint ID tokens. A client
that read a hardcoded RS256 and requested openid would reach
Application.jwk_key on a client registered with no algorithm and take
an ImproperlyConfigured 500, after logging in and consenting. The list
is empty when no RSA key is configured.
A backend that hosts no authorization server raises NotImplementedError,
surfaced as 501 for parity with
AuthorizationServerMetadataViewSet.
DynamicClientRegistrationViewSet ¶
Bases: ViewSet
RFC 7591 Dynamic Client Registration endpoint.
Deprecated by the MCP specification, and kept until the specification
removes it. Revision 2026-07-28 deprecated Dynamic Client Registration
in favour of Client ID Metadata Documents, and the spec's deprecated-features
registry gives it an earliest removal of the first revision released on or
after 2027-07-28. Deprecated is not removed: DCR remains a MAY in the
current revision, and this endpoint is supported for as long as that holds.
The replacement is CIMD, which is an authorization-server capability rather
than one of ours — on django-oauth-toolkit 3.4 and later,
OAUTH2_PROVIDER["CIMD_ENABLED"] = True is the whole configuration, and
AuthorizationServerMetadata.client_id_metadata_document_supported then
advertises it. New deployments should reach for that first; a client that
reads it never reaches this endpoint, because the spec's registration
priority order puts pre-registration and CIMD above DCR.
Why the surface stays, and why nothing warns at runtime. Most MCP
clients in the field have no CIMD implementation at all, and several have no
alternative to DCR whatsoever, so a server that withdraws this endpoint
ahead of the timetable is a server those clients simply cannot connect to.
A deployment reaching this code has already opted in twice — include_dcr
at the mount and dcr_enabled on top of it, both defaulting off — so a
DeprecationWarning here would fire on a correct configuration whose
operator has nothing to act on, and it would fire once per registration
request, since this package forbids the warn-once module state that would
quieten it. An announcement that cannot be acted on trains people to filter
the module, which is worse than no announcement. It belongs where the
decision is made — this docstring, the settings reference, and the
authentication guide — not on every request after it.
Locked down by default: dcr_enabled=False answers 403 to every
request. Turn it on with REST_FRAMEWORK_MCP['DCR_ENABLED'] and,
recommended, a DCR_INITIAL_ACCESS_TOKEN clients must present. Wired as
the create action: as_view({"post": "create"}).
A successful POST persists a DOT Application and returns the RFC 7591
client information response — client_id, the registered metadata, and a
plaintext client_secret for confidential clients. Registering with
token_endpoint_auth_method: none makes a public client, issued no
secret and authenticating with PKCE alone, which is the only mode some
connectors can use.
DOT is imported lazily inside the action, so this module stays importable
without the [oauth] extra and a request arriving with DCR enabled but
DOT absent gets a clear ImportError rather than a startup failure.
DRF's default auth / permission / throttling layers are off: DCR is gated
by its own knobs, not by DRF's authenticators. CSRF is sidestepped because
APIView.dispatch applies csrf_exempt semantics when no
SessionAuthentication is configured.
DynamicClientRegistrationSerializer ¶
Bases: DataclassSerializer
RFC 7591 dynamic client registration request shape.
Dynamic Client Registration is deprecated by MCP revision 2026-07-28 in
favour of Client ID Metadata Documents; the timetable, the replacement and
the reason this surface is nonetheless kept are recorded once, on
DynamicClientRegistrationViewSet.
Wraps
DynamicClientRegistrationRequest
so .save() hands
DynamicClientRegistrationViewSet
a typed dataclass instance. The field overrides replace the dataclass-derived
defaults with shapes that validate the wire contract: redirect_uris is required,
non-empty and absolute-URI-valued, and application_type is checked against
OIDC's two values and echoed without imposing the redirect-URI constraints an OIDC
provider would derive from it.
token_endpoint_auth_method / grant_types are the RFC 7591 §2
spellings every interoperable client sends, and are the primary inputs:
validate translates them into DOT's client_type /
authorization_grant_type. Those two remain accepted as an escape hatch
for callers already speaking DOT, their choices sourced from
Application's constants at instance construction — so a malformed value
is rejected per-field before reaching the database, and the lazy import
keeps this module usable without the [oauth] extra.
Supplying both spellings is allowed only when they agree. A contradiction is a 400 rather than a silent winner, since either choice would hand back a client that cannot complete the flow it registered for. Other RFC 7591 fields are ignored: DOT does not model them, and inventing a richer shape would diverge from the underlying authorization server.
authorization_code is the only primary grant registerable here, in
either vocabulary — see _REGISTERABLE_GRANT_TYPES. refresh_token may
ride along.
validate_redirect_uris ¶
Require absolute URIs whose scheme the authorization server will honour.
DRF's URLField was the wrong instrument: its URLValidator
allowlists the http family, so it refused exactly the private-use
schemes (com.example.app:/oauth2redirect) that RFC 8252 §7.1 defines
for the native clients this serializer's own application_type exists
to describe, while admitting ftp, which no OAuth client redirects to.
The authority on which schemes are acceptable is the authorization
server that will later match the redirect, not this endpoint: DOT
publishes it as ALLOWED_REDIRECT_URI_SCHEMES, defaulting to the http
family and widened by the operator who deploys native clients. Checking
the same list here means a registration is refused only where the
authorization request would have been refused anyway — with an
actionable 400 at registration rather than a dead end mid-flow — and
never invents a restriction DOT does not apply.
validate ¶
Reconcile the RFC 7591 and DOT spellings, in both directions.
Downstream reads all four fields already populated and mutually consistent, so nobody has to know which vocabulary the client used and nobody re-applies defaults.
attrs is the dataclass DataclassSerializer.to_internal_value
built, mutable precisely so normalisation can happen in place. Omitted
fields carry the dataclass defaults ("" / []), neither of which
is an accepted choice, so "empty" unambiguously means "not supplied".
AuthorizationServerMetadata
dataclass
¶
RFC 8414 OAuth 2.0 Authorization Server Metadata payload.
Returned by MCPAuthBackend.authorization_server_metadata and
serialised by the contrib AS metadata ViewSet. A backend that hosts no
authorization server raises NotImplementedError instead, which
that ViewSet maps to 501 Not Implemented.
Field shapes mirror RFC 8414. The str-typed endpoints default to ""
so the wire shape is valid JSON even when the configuration is incomplete;
populate SERVER_INFO to fill them.
client_id_metadata_document_supported
class-attribute
instance-attribute
¶
Whether the authorization server accepts an HTTPS URL as a client_id.
Clients check this to decide how to register: the priority order is pre-registration, then CIMD, then the deprecated Dynamic Client Registration, so a server that supports CIMD but stays silent sends every client down the deprecated path.
Never hardcode this to True. It describes the authorization server, not
this package, so a backend must source it from what the AS actually does or
the advertisement drifts from the behaviour.
OpenIDDiscoveryPayload
dataclass
¶
OIDC discovery alias payload — extends
AuthorizationServerMetadata.
Composes rather than subclasses the AS metadata, so the underlying type stays
exactly RFC 8414. The OIDC additions are advertised because some MCP hosts probe
/.well-known/openid-configuration first and skip the probe silently when these
keys are absent. See
OpenIDDiscoveryViewSet
for why OIDC-shaped metadata is returned with no ID-token endpoint behind it.
DynamicClientRegistrationRequest
dataclass
¶
RFC 7591 dynamic client registration request payload.
Mutable, so
DynamicClientRegistrationSerializer
can normalise in place; frozen would force a second instance just to change a
defaulted field.
Two vocabularies land here side by side. token_endpoint_auth_method,
grant_types and response_types are the RFC 7591 §2 fields an
interoperable client sends; client_type and
authorization_grant_type are DOT's non-standard equivalents, kept as an
escape hatch. The serializer's validate reconciles them, so everything
downstream reads a consistent set.
response_types has no DOT counterpart: RFC 7591 §2.1 makes it a
function of the grant, so it is derived rather than stored and an explicit
value contradicting the grant is rejected.
id_token_signed_response_alg maps to Application.algorithm, which
decides whether an ID token can be signed at all; only the viewset can
resolve it, because usability depends on server configuration (an RSA key)
and client type (HS256 signs with a secret a public client does not have).
application_type is validated and echoed but has no DOT counterpart —
the MCP spec makes sending it a client MUST, so dropping it silently
would leave a client that declared native unable to tell whether it had
been heard.
RFC 7591 fields the server does not understand (contacts,
logo_uri, jwks, …) are ignored, as §2 requires: DOT has nowhere to
put them, and echoing metadata the authorization server will not honour is
worse than dropping it.
DynamicClientRegistrationResponse
dataclass
¶
RFC 7591 client information response.
The wire shape
DynamicClientRegistrationViewSet
returns on a successful registration.
RFC 7591 §3.2.1 lets the authorization server substitute any metadata value it likes but obliges it to return everything it registered, so every field here is the resolved value rather than an echo of the request: an untold substitution turns a legal downgrade into an undiagnosable failure, with the client behaving as what it asked to be while the token endpoint enforces something else.
client_secret is the plaintext secret and is present only for
confidential clients — DOT hashes the column on save, so this has to be the
value generated before the Application was written; read back off the
model it would be the PBKDF2 digest, which no client can authenticate with.
A public client gets no secret at all, per RFC 7591 §2.
client_secret_expires_at rides along whenever one is issued, §3.2.1
making it REQUIRED in that case, with 0 meaning "does not expire".
scope is emitted only when the request supplied one.
DCR is deprecated by MCP revision 2026-07-28, in favour of Client ID
Metadata Documents, with an earliest removal of the first specification revision
released on or after 2027-07-28. It is kept, and stays off by default; the
timetable and the reasoning are on DynamicClientRegistrationViewSet above and
in Authentication.
DCR is gated by two build_oauth_urlpatterns arguments, each defaulting to the
matching setting when omitted:
dcr_enabled(default:REST_FRAMEWORK_MCP["DCR_ENABLED"], itselfFalse) — the DCR endpoint refuses every request while disabled.dcr_initial_access_token(default:REST_FRAMEWORK_MCP["DCR_INITIAL_ACCESS_TOKEN"], itselfNone) — optional bearer required on the DCR POST, per RFC 7591 §3.
User-adapter hook (cookie-session bridge for /authorize)¶
AuthUserAdapter ¶
Bases: Protocol
Hydrate request.user before DOT's AuthorizationView dispatches.
DOT's AuthorizationView knows only Django's session-based
request.user, so on the common "DRF backend with SimpleJWT cookies"
setup an authenticated user appears anonymous to the OAuth flow and is
shown the consent screen again. The adapter is the seam where the
consumer's own authentication scheme decides which user the flow should
attribute the grant to.
hydrate returns the authenticated user to set on the request before
delegating to DOT, or None to leave request.user untouched — DOT
then falls back to its session-based flow, which may redirect to login.
Implementations MUST be safe to instantiate without arguments.
SimpleJWTCookieAdapter ¶
Reference
AuthUserAdapter
for SimpleJWT cookie-authenticated apps.
Reads the access-token cookie (cookie_name=, defaulting to
REST_FRAMEWORK_MCP['SIMPLEJWT_ACCESS_COOKIE']), decodes it with
rest_framework_simplejwt.tokens.AccessToken and looks the user up
by primary key. Every failure mode — no cookie, malformed or expired
token, unknown user — returns None, so DOT's view falls back to its
session-based flow.
rest_framework_simplejwt is imported lazily inside hydrate, so
this module stays importable without the [jwt] extra and a consumer
who configures the adapter without it gets a clear ImportError at first
request rather than at import.