Skip to content

OAuthProxy: CIMD clients get required_scopes instead of valid_scopes as default #3828

@seth-with-zest

Description

@seth-with-zest

Bug

When a CIMD client (e.g., Claude Code with client_id=https://claude.ai/oauth/claude-code-client-metadata) connects to an OAuthProxy/GoogleProvider server, it gets registered with only required_scopes instead of valid_scopes. This causes the MCP SDK's validate_scope to reject authorization requests with:

invalid_scope: Client was not registered with scope https://www.googleapis.com/auth/calendar

The error never reaches the upstream IdP — it's rejected at the proxy's own scope validation before building the upstream authorization URL.

Root Cause

In OAuthProxy.__init__ (proxy.py:363):

self._default_scope_str: str = " ".join(self.required_scopes or [])

This is used in two places as the fallback when a client doesn't specify scopes:

  1. CIMDClientManager.__init__ — receives default_scope=self._default_scope_str, and get_client() uses it: scope=cimd_doc.scope or self.default_scope
  2. register_client — uses it directly: scope=client_info.scope or self._default_scope_str

For case 2, the MCP SDK's RegistrationHandler applies default_scopes from ClientRegistrationOptions before calling register_client, so client_info.scope is already populated and the fallback is never hit.

For case 1 (CIMD clients), the CIMDClientManager creates clients directly — it never goes through the RegistrationHandler, so ClientRegistrationOptions.default_scopes is never applied. The only fallback is _default_scope_str, which contains required_scopes (e.g., ["openid"]) instead of valid_scopes (e.g., all Google Workspace scopes).

Reproduction

  1. Create a GoogleProvider with required_scopes=["openid"] and valid_scopes=["openid", "https://www.googleapis.com/auth/calendar", ...]
  2. Connect with Claude Code (or any CIMD client whose metadata document has no scope field)
  3. Authorize with any scope beyond openid
  4. Get invalid_scope error — the client was registered with only ["openid"]

Suggested Fix

In OAuthProxy.__init__, prefer valid_scopes over required_scopes for the default scope string:

# Before (line 363):
self._default_scope_str: str = " ".join(self.required_scopes or [])

# After:
self._default_scope_str: str = " ".join(
    valid_scopes or self.required_scopes or []
)

This is consistent with how ClientRegistrationOptions already handles it — valid_scopes represents what clients can request, while required_scopes represents what tokens must have.

Workaround

Override _default_scope_str and the CIMD manager's default_scope after provider creation:

valid_scope_str = " ".join(all_valid_scopes)
provider._default_scope_str = valid_scope_str
if provider._cimd_manager is not None:
    provider._cimd_manager.default_scope = valid_scope_str

Environment

  • fastmcp 3.2.0
  • mcp 1.26.0
  • Client: Claude Code (CIMD client_id: https://claude.ai/oauth/claude-code-client-metadata)

Metadata

Metadata

Assignees

No one assigned

    Labels

    authRelated to authentication (Bearer, JWT, OAuth, WorkOS) for client or server.bugSomething isn't working. Reports of errors, unexpected behavior, or broken functionality.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions