Skip to content

OAuth metadata discovery fails for servers with path components (RFC 8414/9728 compliance) #697

Description

@Dumbris

Summary

OAuth metadata discovery fails for MCP servers that use path components in their URLs or have separate domains for MCP and OAuth services. This affects servers like Smithery that use:

  • MCP Server: https://server.smithery.ai/googledrive
  • Auth Server: https://auth.smithery.ai/googledrive

Problem

1. Base URL Path Stripping

In client/transport/streamable_http.go (lines 163-168), the base URL construction strips the path:

if smc.oauthHandler != nil {
    baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
    smc.oauthHandler.SetBaseURL(baseURL)
}

For https://server.smithery.ai/googledrive, this sets baseURL to https://server.smithery.ai, causing:

  • PRM discovery at https://server.smithery.ai/.well-known/oauth-protected-resource (wrong)
  • Should be: https://server.smithery.ai/.well-known/oauth-protected-resource/googledrive (RFC 9728)

2. RFC 8414 Path Insertion Missing

The current code only tries one metadata URL format:

authServerURL + "/.well-known/oauth-authorization-server"

RFC 8414 Section 3.1 specifies that when the issuer URL contains a path, the well-known path should be inserted between host and path:

  • Input: https://auth.smithery.ai/googledrive
  • Current: https://auth.smithery.ai/googledrive/.well-known/oauth-authorization-server
  • RFC 8414: https://auth.smithery.ai/.well-known/oauth-authorization-server/googledrive

3. No WWW-Authenticate Header Parsing

RFC 9728 Section 3 specifies that servers should advertise their Protected Resource Metadata URL via the WWW-Authenticate header:

WWW-Authenticate: Bearer error="invalid_request", resource_metadata="https://server.smithery.ai/.well-known/oauth-protected-resource/googledrive"

The current implementation doesn't parse this header to extract the resource_metadata URL.

Impact

  • DCR fails with 404 because the registration endpoint URL is constructed incorrectly
  • Authorization URL has empty client_id because DCR failed
  • Affects all servers using path-based URLs or separate auth domains (Smithery, potentially others)

Suggested Fix

1. RFC 8414 Compliant URL Construction

// BuildRFC8414MetadataURLs constructs OAuth Authorization Server Metadata URLs per RFC 8414.
func BuildRFC8414MetadataURLs(authServerURL string) []string {
    u, err := url.Parse(authServerURL)
    if err != nil {
        return []string{authServerURL + "/.well-known/oauth-authorization-server"}
    }

    path := strings.TrimSuffix(u.Path, "/")
    baseURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host)

    var urls []string
    if path == "" || path == "/" {
        urls = append(urls, baseURL+"/.well-known/oauth-authorization-server")
    } else {
        // RFC 8414: insert .well-known between host and path
        rfc8414URL := baseURL + "/.well-known/oauth-authorization-server" + path
        urls = append(urls, rfc8414URL)
        // Legacy fallback
        legacyURL := strings.TrimSuffix(authServerURL, "/") + "/.well-known/oauth-authorization-server"
        urls = append(urls, legacyURL)
        // Base URL fallback (for servers like Cloudflare)
        urls = append(urls, baseURL+"/.well-known/oauth-authorization-server")
    }
    return urls
}

2. WWW-Authenticate Header Parsing

// ExtractResourceMetadataURL parses WWW-Authenticate header for resource_metadata URL
func ExtractResourceMetadataURL(wwwAuthHeader string) string {
    if !strings.Contains(wwwAuthHeader, "resource_metadata") {
        return ""
    }
    parts := strings.Split(wwwAuthHeader, `resource_metadata="`)
    if len(parts) < 2 {
        return ""
    }
    endIdx := strings.Index(parts[1], `"`)
    if endIdx == -1 {
        return ""
    }
    return parts[1][:endIdx]
}

3. Auth Server Discovery from PRM

When the MCP server and OAuth server are on different domains, discover the auth server from Protected Resource Metadata before fetching OAuth metadata.

Related Issue

We encountered this while implementing OAuth support in MCPProxy:

Our workaround implements the discovery logic in mcpproxy, but this would benefit all mcp-go users.

Test Case

Smithery's googledrive server can be used to verify the fix:

# Protected Resource Metadata (RFC 9728)
curl -s "https://server.smithery.ai/.well-known/oauth-protected-resource/googledrive" | jq .
# Returns: {"authorization_servers": ["https://auth.smithery.ai/googledrive"], ...}

# OAuth Authorization Server Metadata (RFC 8414)
curl -s "https://auth.smithery.ai/.well-known/oauth-authorization-server/googledrive" | jq .
# Returns: {"issuer": "...", "registration_endpoint": "...", ...}

# DCR endpoint
curl -s -X POST "https://auth.smithery.ai/googledrive/register" \
  -H "Content-Type: application/json" \
  -d '{"redirect_uris": ["http://127.0.0.1:8080/callback"]}'
# Returns: {"client_id": "...", "client_secret": "..."}

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions