Skip to content

MCP remote transport advertises an OAuth mode it denies: auth_type="oauth" falls through to refuse #3463

Description

@chernistry

RemoteMCPConfig.auth_type documents three values (src/bernstein/mcp/remote_transport.py:269):

auth_type: str = "bearer"  # "none", "bearer", "oauth"

_authenticate (remote_transport.py:1595) implements two of them. "none" admits, "bearer" compares a shared secret in constant time, and everything else reaches:

# Unknown auth type - deny.
return False

So a server configured with auth_type="oauth" — a value the config's own comment presents as supported — refuses every request, including correctly authenticated ones.

That would be a small papercut on its own. What makes it worth an issue is that the rest of the OAuth surface is real and points clients straight at it:

  • src/bernstein/mcp/oauth.py resolves a configured issuer URL and serves protected-resource metadata.
  • _request_base_url (remote_transport.py:1495) exists specifically so the metadata document and the WWW-Authenticate challenge cannot disagree about the URL, and its docstring shows real care about header injection through Host / X-Forwarded-Proto.
  • _unauthorized_response (remote_transport.py:1520) emits an issuer-bearing challenge on every 401, telling the client where to go and get a token.

We therefore advertise a discovery document, emit a challenge naming an authorization server, accept the matching auth_type value in configuration — and then deny the token that comes back. An operator following our own 401 challenge cannot reach a working state.

The security question the fix has to answer, not skip

Implementing "oauth" as "accept any bearer token that parses" would be worse than the current refusal. A token is only meaningful bound to the authorization server that minted it, and to the resource it was minted for:

  • Issuer binding. Nothing in src/bernstein/mcp/ validates an iss today (grep finds the word only in docstrings and in the advertise-side helper). A token minted by a different authorization server the client also talks to must not authenticate here.
  • Audience binding. The token must name this resource, or a token issued for an unrelated service authenticates against ours.
  • Failure shape. A token that fails either check must produce the same 401-with-challenge as no token at all, and must not leak which check failed.

These are the checks that make the difference between "OAuth is configured" and "OAuth is enforced," and they are the reason this is a size/m rather than a five-line branch.

Acceptance criteria

  • auth_type="oauth" either authenticates correctly or is removed from the config's documented values. A value the code advertises and unconditionally denies is not acceptable in either direction.
  • If implemented: a presented token is rejected unless its issuer matches the configured issuer and its audience names this resource.
  • A token from a different issuer, and a token whose audience names another resource, are each rejected with the same response shape as no credential at all — the refusal does not distinguish them.
  • The discovery document, the WWW-Authenticate challenge, and the accepted issuer are derived from one configured value, so they cannot drift apart. _request_base_url already establishes this pattern for the first two.
  • Tests fail before and pass after, including the wrong-issuer and wrong-audience cases — demonstrate both, do not assert them.
  • docs/ states which auth modes the remote transport supports and what each proves.

Not in scope

Stateless serving is already done (#2506): the transport keeps no per-client session, and the legacy protocol-session header is a dated no-op shim. This issue does not touch that.

Effort

M. The branch is small; the issuer and audience binding, and getting the refusal shape uniform, is the work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-welcomeSuitable for AI agent contributorsbugSomething isn't workinghelp wantedExtra attention is neededsecuritySecurity hardeningsize/mup-for-grabsListed on up-for-grabs.net — no commitment, low friction

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions