Skip to content

Bug: Dynamic Client Registration Requires Both authorization_code and refresh_token Grant Types #2460

@gazzadownunder

Description

@gazzadownunder

Description

Summary

The Dynamic Client Registration (DCR) handler is rejecting valid RFC 7591-compliant registration requests that only specify authorization_code in the grant_types field. This is overly restrictive and breaks compatibility with OAuth clients that follow the RFC 7591 specification.

Location

File: mcp/server/auth/handlers/register.py
Line: 71-78

Current Behavior

The registration handler validates that both authorization_code and refresh_token grant types must be present:

if not {"authorization_code", "refresh_token"}.issubset(set(client_metadata.grant_types)):
    return PydanticJSONResponse(
        content=RegistrationErrorResponse(
            error="invalid_client_metadata",
            error_description="grant_types must be authorization_code and refresh_token",
        ),
        status_code=400,
    )

Result: HTTP 400 Bad Request with error:

{
  "error": "invalid_client_metadata",
  "error_description": "grant_types must be authorization_code and refresh_token"
}

Expected Behavior

Per RFC 7591 (OAuth 2.0 Dynamic Client Registration Protocol), the grant_types field should accept any valid OAuth 2.0 grant type, and refresh_token is optional, not required.

The handler should accept registration requests with only authorization_code in grant_types:

{
  "client_name": "MCP Compliance Tester",
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

Expected Result: HTTP 201 Created with client credentials

RFC 7591 Compliance

From RFC 7591 Section 2:

grant_types: Array of OAuth 2.0 grant type strings that the client can use at the token endpoint. [...] If omitted, the default is that the client will use only the "authorization_code" Grant Type.

The specification does not mandate that refresh_token must be included. In fact, it's listed as a separate, optional grant type.

Impact

This restrictive validation:

  1. Breaks RFC 7591 compliance - Rejects valid registration requests
  2. Fails MCP OAuth compliance tests - Specifically test DCR-3.2 (HTTP 201 Registration Response)
  3. Limits interoperability - Clients that don't want refresh token support cannot register
  4. Forces unnecessary capabilities - Not all OAuth flows require refresh tokens

Reproduction

Test Request

curl -X POST http://localhost:3010/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Test Client",
    "redirect_uris": ["http://localhost:3000/callback"],
    "grant_types": ["authorization_code"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none"
  }'

Actual Response

{
  "error": "invalid_client_metadata",
  "error_description": "grant_types must be authorization_code and refresh_token"
}

Status: 400 Bad Request

Expected Response

{
  "client_id": "550e8400-e29b-41d4-a716-446655440000",
  "client_id_issued_at": 1234567890,
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "client_name": "Test Client"
}

Status: 201 Created

Proposed Fix

Change the validation at line 71 to only require authorization_code:

# Before (overly restrictive):
if not {"authorization_code", "refresh_token"}.issubset(set(client_metadata.grant_types)):
    return PydanticJSONResponse(
        content=RegistrationErrorResponse(
            error="invalid_client_metadata",
            error_description="grant_types must be authorization_code and refresh_token",
        ),
        status_code=400,
    )

# After (RFC 7591 compliant):
if "authorization_code" not in client_metadata.grant_types:
    return PydanticJSONResponse(
        content=RegistrationErrorResponse(
            error="invalid_client_metadata",
            error_description="grant_types must include 'authorization_code'",
        ),
        status_code=400,
    )

This change:

  • ✅ Accepts ["authorization_code"] (RFC compliant)
  • ✅ Accepts ["authorization_code", "refresh_token"] (still works)
  • ✅ Rejects requests without authorization_code (maintains MCP requirement)
  • ✅ Allows clients to opt-out of refresh tokens if not needed

Functional Impact Analysis

I've analyzed the downstream dependencies and confirmed:

No Breaking Changes to Core Functionality

The rest of the MCP SDK and FastMCP OAuthProxy already handle optional refresh tokens correctly:

  1. Token Exchange (exchange_authorization_code):

    • Only issues refresh tokens if upstream provider returns one
    • Gracefully returns refresh_token: null if not available
  2. Token Storage:

    • UpstreamTokenSet.refresh_token is already typed as str | None
    • All conditional checks use if idp_tokens.get("refresh_token")
  3. Token Refresh Flow:

    • exchange_refresh_token() method only called if client has refresh token
    • Returns proper error if refresh not supported: "Refresh not supported for this token"
  4. Client Behavior:

    • Clients without refresh tokens simply re-authenticate when access token expires
    • This is standard OAuth 2.0 behavior and doesn't break functionality

📊 What Works vs What Doesn't

Works with grant_types: ["authorization_code"] only:

  • ✅ Client registration
  • ✅ Authorization flow
  • ✅ Token exchange (returns access token)
  • ✅ Access token validation
  • ✅ Token revocation
  • ✅ Re-authentication when access token expires

Doesn't work (expected):

  • ❌ Token refresh endpoint (client must re-authorize instead)

This is standard OAuth 2.0 behavior - not all clients need refresh tokens.

Additional Context

  • MCP Specification: Requires authorization code flow with PKCE (satisfied by requiring authorization_code)
  • Refresh Tokens: Should be optional, not mandatory
  • Backward Compatibility: This fix is backward compatible - clients currently sending both grant types will continue to work
  • Downstream Code: FastMCP OAuthProxy already implements optional refresh token support correctly

Environment

  • MCP SDK: Latest version (as of 2025-11-20)
  • Python: 3.12.8
  • OAuth Provider: FastMCP OAuthProxy pattern
  • Test Framework: MCP OAuth Compliance Tester

References

Example Code

Version Information

FastMCP version:                                                                                      2.13.1
MCP version:                                                                                          1.21.0
Python version:                                                                                       3.12.8
Platform:                                                                          Windows-11-10.0.26100-SP0

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.serverRelated to FastMCP server implementation or server-side functionality.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions