Skip to content

fix(auth): no error code for a request that presented no credentials - #3824

Closed
AnnasMazhar wants to merge 1 commit into
modelcontextprotocol:mainfrom
AnnasMazhar:fix/no-credential-401-scope
Closed

AnnasMazhar wants to merge 1 commit into
modelcontextprotocol:mainfrom
AnnasMazhar:fix/no-credential-401-scope

Conversation

@AnnasMazhar

Copy link
Copy Markdown

Summary

RequireAuthMiddleware collapses two different failures into one challenge, and never advertises the required scope.

1. A request that presented no credentials is answered with an error code. RFC 6750 Section 3.1: a request with no credentials SHOULD NOT include an error code. The gate answered a no-Authorization request with error="invalid_token" — the same challenge as a token that was presented and rejected.

2. No challenge ever carried scope. The spec SHOULD include it for both the discovery-time 401 (#protected-resource-metadata-discovery-requirements) and the runtime 403 (#runtime-insufficient-scope-errors). This SDK's own client reads scope out of the header to drive step-up (utils.py extract_scope_from_www_auth), so the gap was a resource-server/client asymmetry: the client consumed a parameter the server never sent.

Changes

src/mcp/server/auth/middleware/bearer_auth.py

  • __call__: the not-authenticated branch now checks whether an Authorization header was actually presented. If not, the challenge carries no error code and the body is empty; if a credential was presented and rejected, error="invalid_token" is kept. Either way the challenge advertises the required scope.
  • The 403 branch now passes the required scope so it is named in the challenge.
  • _send_auth_error takes an optional scope, emits it as a scope="..." parameter, and allows error/description to be absent.

The scope string is joined outside the f-string: nested same-type quotes in an f-string need PEP 701 (Python 3.12+), and this project supports 3.11.

Resulting behaviour

request status WWW-Authenticate body
no Authorization 401 resource_metadata, scope empty
invalid token 401 error="invalid_token", resource_metadata, scope {"error": ..., "error_description": ...}
insufficient scope 403 error="insufficient_scope", scope, resource_metadata {"error": ..., "error_description": ...}

Tests

tests/interaction/auth/test_bearer.py and tests/docs_src/test_authorization.py updated to pin the corrected challenge (the previous ones pinned the old one via inline_snapshot). tests/interaction/_requirements.py drops the now-resolved divergence records on hosting:auth:missing-401, hosting:auth:scope-403 and hosting:auth:invalid-401.

docs/run/authorization.md updated so the tutorial shows the two cases separately.

Local run: 276 passed, 1 xfailed (tests/interaction/auth/, tests/interaction/test_coverage.py, tests/client/test_auth.py, tests/docs_src/test_authorization.py); ruff check and ruff format --check clean.

Note for the reviewer

The maintainer comment on the issue suggests the backend should signal "no credentials" distinctly (e.g. a sentinel/exception) rather than the header check used here. The header check gets the same split with a smaller diff, but I am happy to switch to backend signalling if that is preferred. The scope value is the scopes required for the current request — self.required_scopes when the middleware is instantiated per route — not settings.required_scopes blindly.

Closes #3769

RFC 6750 Section 3.1 says a request that carried no credentials SHOULD NOT be
answered with an error code, but the bearer gate collapsed the no-header case
into the same challenge as an invalid token and emitted error="invalid_token".
The challenge also never carried the `scope` parameter, which the spec SHOULD
include for both the discovery-time 401 and the runtime 403 -- and which this
SDK's own client reads (utils.py extract_scope_from_www_auth) to drive
step-up, so the gap was a resource-server/client asymmetry.

Split the two cases:

* no Authorization header -> 401 with resource_metadata and scope, no error
  code, and an empty body
* presented but invalid token -> 401 with error="invalid_token", plus scope
* insufficient scope -> 403 with error="insufficient_scope" and the required
  scope named

The scope parameter is optional on _send_auth_error and is joined outside the
f-string: nested same-type quotes in an f-string need PEP 701 (3.12+) and this
project supports 3.11.

Resolves the divergences recorded on hosting:auth:missing-401,
hosting:auth:scope-403 and hosting:auth:invalid-401; those entries no longer
carry a divergence, and the tests that pinned the old challenge now pin the
corrected one.

Closes modelcontextprotocol#3769
@github-actions github-actions Bot added the missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md) label Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3769.

If a maintainer assigns you to #3769, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take.

You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way.

CONTRIBUTING.md has the full reasoning, but in short:

  • We're a small team with very little capacity to review community PRs right now.
  • Many recent PRs are AI-generated with little human review, and reviewing one carefully still costs a maintainer as much time as it ever did. A well-described issue is usually more useful to us than the code.

Maintainers: reopen, remove missing-issue-link, or add bypass-issue-check to override.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

401 and error for a request with no authorization header (for a request with no token) - as per spec no error should be added

1 participant