Skip to content

CIMD redirect_uri validation should accept any port on loopback addresses (RFC 8252) #3598

@PrithviSriram

Description

@PrithviSriram

Summary

FastMCP 3.0.2's CIMD redirect URI validation strictly matches ports on loopback addresses, rejecting dynamic-port callbacks from native OAuth clients like Claude Code. Per RFC 8252 §7.3, authorization servers MUST allow any port for loopback redirect URIs.

Reproduction

  1. A CIMD metadata document lists http://localhost/callback as a redirect URI (no port = port 80):

    {
      "client_id": "https://claude.ai/oauth/claude-code-client-metadata",
      "redirect_uris": ["http://localhost/callback", "http://127.0.0.1/callback"]
    }
  2. The OAuth client (Claude Code) starts a callback listener on a dynamic ephemeral port and sends:

    redirect_uri=http://localhost:51212/callback
    
  3. FastMCP's _match_port() in fastmcp/server/auth/redirect_validation.py normalizes the missing port to "80" and does a strict comparison:

    uri_effective = uri_port if uri_port else default_port   # "51212"
    pattern_effective = pattern_port if pattern_port else default_port  # "80"
    return uri_effective == pattern_effective  # False — rejected
  4. The authorize flow raises:

    Redirect URI 'http://localhost:51212/callback' does not match CIMD redirect_uris.
    

Expected behavior

For loopback addresses (localhost, 127.0.0.1, [::1]), _match_port() should accept any port when the pattern port is the default (or absent), per RFC 8252:

The authorization server MUST allow any port to be specified at the time of the request for loopback IP redirect URIs, to accommodate clients that obtain an available ephemeral port from the operating system at the time of the request.

Suggested fix

In _match_port(), add a loopback check before the strict comparison:

def _match_port(
    uri_port: str | None,
    pattern_port: str | None,
    uri_scheme: str,
    uri_host: str | None = None,  # new parameter
) -> bool:
    if pattern_port == "*":
        return True

    # RFC 8252 §7.3: accept any port on loopback addresses
    loopback_hosts = {"localhost", "127.0.0.1", "::1"}
    if uri_host and uri_host.lower() in loopback_hosts:
        return True

    default_port = "443" if uri_scheme == "https" else "80"
    uri_effective = uri_port if uri_port else default_port
    pattern_effective = pattern_port if pattern_port else default_port
    return uri_effective == pattern_effective

The caller (matches_allowed_pattern) already has uri_host parsed and can pass it through.

Impact

This blocks CIMD adoption for any native MCP client using dynamic loopback ports (which is the standard pattern for CLI/desktop OAuth clients). We had to disable CIMD on all our FastMCP-based MCP servers and fall back to DCR as a workaround.

Environment

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.high-prioritypotential-duplicateBot-suggested duplicate awaiting human review. Auto-closes after 3 days if unchallenged.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions