v0.21.0
Fixed
-
DjangoOAuthToolkitBackendrejected every bearer token as soon as a resource
URL was configured, and enforcement could not be turned off. Audience
enforcement was implied byresource_urlalone, and it reads the token's bound
resource viagetattr(token, "resource", None)— but DOT'sAccessTokenhas
noresourcefield and DOT implements no RFC 8707 resource indicators at all,
so that is alwaysNone.audience_matches(None, "https://…")isFalse, so
authenticatereturnedNonefor every valid token. Confirmed against DOT
3.3.0: the model's fields areapplication, created, expires, id, id_token, refresh_token, scope, source_refresh_token, token, token_checksum, updated, user.Worse, the condition was inescapable.
resource_url=""counted as "not None",
so it skipped the settings fallbacks and enforced against the empty string;
and clearing the setting entirely made PRM advertiseresource: "", which RFC
9728 marks REQUIRED. A deployment had to choose between authenticating anybody
and publishing valid metadata — the MCP spec requires the latter, so the
bundled OAuth backend was unusable in any real deployment.ENFORCE_AUDIENCEis now a separate setting, defaultFalse.
resource_urlmeans "the identity this server publishes"; enforcement means
"reject tokens that don't carry it". Coupling them was the bug.audience_getter=says where the audience actually lives — a JWT claim,
a gateway header, a related row. The default still readstoken.resource,
which is meaningful for a swapped
OAUTH2_PROVIDER["ACCESS_TOKEN_MODEL"]carrying that field.- Turning enforcement on without a usable audience source raises
ImproperlyConfiguredat construction — which is startup, since
MCPServer.__init__builds the backend — naming both ways out. A server that
rejects everything is a configuration error; discovering it as a per-request
401 is what made this take a live deployment to find. resource_url=""now means unset at every layer, and unconfigured PRM carries
a_warningexplaining the emptyresourceinstead of leaving a blank
required field.
Not a security regression. The previous default was on-but-unsatisfiable,
so no deployment can have been relying on it working — it rejected valid and
invalid tokens alike.Why the suite missed it — sharper than "no resource URL was configured in
the tool-call suites. The backend's own audience tests driveauthenticate
through a_FakeTokenthat has aresourceattribute DOT's real model does
not. The fake was more capable than the thing it stood for, so the audience
path looked thoroughly covered while being dead in production. The new
tests/auth/backends/test_dot_backend_audience_reality.pyuses a genuine
AccessTokenrow, and pins"resource" not in AccessTokenso the divergence
cannot silently return; the fake-token tests now pass an explicit
audience_getterand say why.
Documentation
- Removed
OAUTH2_PROVIDER["REQUIRE_RESOURCE"]from the DOT recipe. That
setting does not exist in django-oauth-toolkit —grep -rn REQUIRE_RESOURCE oauth2_provider/finds nothing in 3.3.0 — and the surrounding prose claimed
DOT "handles" forwarding theresourceparameter to the token when it is set.
It does not. An operator following that recipe configured a no-op and then hit
the 401 above. - The mcp-inspector troubleshooting table listed this exact symptom ("Token
accepted but every call still 401") and blamed the authorization server for not
bindingresource. The row now names the real cause.