Back to Mcpproxy Go

Implementing OAuth Authentication for Model Context Protocol Servers in Go: Security, Implementation, and Best Practices

docs/mcp-go-oauth.md

0.38.120.6 KB
Original Source

Implementing OAuth Authentication for Model Context Protocol Servers in Go: Security, Implementation, and Best Practices

This report provides a comprehensive technical analysis of implementing OAuth authentication flows with Model Context Protocol (MCP) servers using Go libraries. We examine redirect URI handling, Dynamic Client Registration (DCR), port allocation strategies, OAuth endpoint discovery, RFC 8252 compliance, practical Go implementations, and common pitfalls. The integration of OAuth 2.1 with MCP enables secure, user-consented access for AI agents to tools and data while adhering to IETF standards. Our investigation reveals that Go libraries like mcp-golang and mcp-go provide robust frameworks for implementing standards-compliant MCP servers with OAuth, though developers must carefully handle callback URIs, PKCE, and token management to avoid security vulnerabilities.

1. Redirect URI Handling Strategies for Local OAuth Flows

1.1 Loopback Interface Implementation

For native applications like CLI-based MCP clients, RFC 8252-compliant loopback redirects are essential. This approach binds a temporary HTTP server to the loopback interface (IPv4 127.0.0.1 or IPv6 ::1) using an OS-assigned ephemeral port. The authorization server must accept any port number since clients cannot predetermine available ports. In Go implementations, libraries like fastmcp automate this by launching a local callback server during OAuth flows, capturing authorization codes after user consent. A critical consideration is preventing port conflicts: Go's net/http package allows binding to port 0 to auto-assign available ports, though this requires coordination with authorization servers to allow dynamic redirect URIs.

1.2 URI Scheme and Security Considerations

Private-use URI schemes (e.g., com.example.app:/oauth2redirect) provide an alternative for desktop environments but introduce security risks from scheme hijacking. RFC 8252 mandates that authorization servers validate redirect URIs against registered patterns, rejecting mismatched ports or schemes. MCP clients should prioritize loopback over private schemes due to deterministic origin validation. For enhanced security, PKCE (Proof Key for Code Exchange) binds authorization requests to specific clients, mitigating interception attacks even if redirect URIs are compromised.

1.3 Implementation in Go Libraries

The mcp-golang library simplifies redirect handling through its transport/http module. Developers declare authorized redirect URIs during server registration, while the client's OAuth helper automatically manages browser interactions and code capture. For Cloudflare Workers-based MCP servers, Stytch's implementation validates redirect URIs against pre-registered patterns, rejecting unauthorized ports or domains.

2. Dynamic Client Registration Best Practices

2.1 Protocol Implementation

RFC 7591-compliant DCR enables MCP clients to self-register with authorization servers at runtime. The client sends a POST request to the /register endpoint with JSON metadata including client_name, scopes, and redirect_uris. The authorization server responds with a unique client_id and client_secret (if applicable). MCP implementations should include the registration_access_token for future client metadata updates, enabling self-service management.

2.2 Security and Validation

To prevent malicious registrations, MCP servers should require initial access tokens during registration, issued through out-of-band mechanisms. Metadata validation must enforce:

  • Scope restrictions limiting client permissions
  • Redirect URI pattern whitelisting
  • Software statement assertions for authenticity

The godoc-mcp server exemplifies this by binding client registrations to PKCE-enhanced OAuth flows, ensuring only user-authorized agents gain access.

2.3 Go Implementation Patterns

The mcp-go library supports DCR through its DynamicClientRegistration() method, which constructs RFC 7591-compliant requests. Developers supply client metadata, with optional JWT software statements for attested identity:

go
metadata := map[string]interface{}{
  "client_name": "AI Agent",
  "scope": "tasks:read tasks:write"
}
resp, err := client.Register(metadata, initialAccessToken)

Post-registration, the client uses issued credentials for subsequent OAuth flows, eliminating pre-registration bottlenecks.

3. Port Allocation and Callback Handling

3.1 Ephemeral Port Management

For local callback servers, Go implementations should:

  1. Bind to localhost:0 to auto-assign ports
  2. Pass the derived port to redirect URIs
  3. Handle OS port conflicts via retries

As RFC 8252 notes, servers must accept any loopback port, requiring MCP servers to validate URIs using pattern matching (e.g., http://127.0.0.1:*) rather than exact matches.

3.2 Cloudflare Workers Approach

Stytch's MCP implementation offloads callback handling to Cloudflare Workers, which:

  • Generate redirect URIs with fixed domains but dynamic paths
  • Validate URI ownership via cryptographic signatures
  • Eliminate local port conflicts entirely

This server-side strategy simplifies client implementation but requires public endpoints.

3.3 Go Client Implementation

The fastmcp client demonstrates robust port handling:

go
func StartCallbackServer() (string, error) {
  listener, _ := net.Listen("tcp", "127.0.0.1:0")
  port := listener.Addr().(*net.TCPAddr).Port
  go http.Serve(listener, callbackHandler)
  return fmt.Sprintf("http://127.0.0.1:%d/callback", port), nil
}

This dynamically assigns ports, passing the URI to OAuth requests while handling conflicts.

4. OAuth Endpoint Discovery

4.1 Metadata Document Standards

RFC 8414 defines the /.well-known/oauth-authorization-server endpoint for disclosing OAuth configuration. MCP servers must publish:

  • authorization_endpoint
  • token_endpoint
  • registration_endpoint
  • jwks_uri

Clients retrieve this document to configure OAuth flows dynamically.

4.2 MCP-Specific Discovery Patterns

The Model Context Protocol specification requires MCP servers to expose OAuth metadata via:

  • GET /.well-known/oauth-authorization-server
  • Including mcp_version and tool_endpoints in responses

This enables clients like godoc-mcp to auto-configure without hardcoded endpoints.

4.3 Go Implementation

The mcp-golang library automates discovery:

go
func DiscoverOAuthConfig(serverURL string) (*OAuthMetadata, error) {
  resp, _ := http.Get(serverURL + "/.well-known/oauth-authorization-server")
  var metadata OAuthMetadata
  json.NewDecoder(resp.Body).Decode(&metadata)
  return &metadata, nil
}

This retrieves endpoints for dynamic client setup, supporting zero-configuration MCP deployments.

5. RFC 8252 Compliance in MCP

5.1 Native App Requirements

RFC 8252 mandates that for native apps (e.g., MCP CLI clients):

  • Use loopback redirects over private URI schemes
  • Implement PKCE to prevent code interception
  • Allow arbitrary ports for redirect URIs

MCP servers violate compliance if they:

  • Reject dynamic ports
  • Require pre-registered exact URIs

5.2 Security Implications

Non-compliance risks:

  • Authorization code interception via URI scheme hijacking
  • Port collision failures in multi-tenant environments

The Sigstore project's OIDC implementation faced vulnerabilities until adopting RFC 8252's port flexibility.

5.3 Go Compliance Patterns

The mcp-go library enforces compliance through:

  • Mandatory PKCE for all OAuth flows
  • Loopback-only redirects in native apps
  • Dynamic port binding with randomized state parameters

Clients should validate server compliance during discovery by checking issuer and token_endpoint fields.

6. Go Implementation Examples

6.1 Full OAuth-MCP Integration

The mcp-golang library provides an OAuth-integrated server:

go
// Server-side
server := mcp.NewServer(transport)
server.RegisterOAuthProvider("google", OAuthConfig{
  ClientID:     "...",
  ClientSecret: "...",
  DiscoveryURL: "https://accounts.google.com/.well-known/openid-configuration",
})

// Client-side
client := mcp.NewClient(transport)
token, err := client.AuthenticateOAuth("google", "https://mcp-server/tasks")

This handles discovery, DCR, and token management automatically.

6.2 Cloudflare Workers Deployment

Stytch's Go-based MCP server uses Workers for OAuth:

go
func handleAuthorize(w http.ResponseWriter, r *http.Request) {
  issuer := "https://oauth.example.com"
  metadata := DiscoverOAuthMetadata(issuer)
  redirectURI := BuildRedirectURI(r, metadata)
  http.Redirect(w, r, metadata.AuthEndpoint+"?response_type=code&..."+redirectURI, 302)
}

This serverless pattern delegates token issuance while retaining MCP tool routing.

6.3 Dynamic Client Registration

The mcp-go DCR workflow:

go
registrationRequest := DynamicRegistrationRequest{
  ClientName: "TodoBot",
  Scope: "tasks:read tasks:write",
}
resp, _ := client.Register(registrationRequest, initialAccessToken)
client.SetCredentials(resp.ClientID, resp.ClientSecret)

This enables runtime onboarding of AI agents.

7. Common Pitfalls and Solutions

7.1 Redirect URI Mismatch

Problem: Authorization servers reject dynamically generated URIs.

Solution:

  • Configure auth servers with wildcard redirect patterns (http://127.0.0.1:*)
  • Use Cloudflare Workers for fixed-domain callbacks

Go Code:

go
// Auth server config
AllowedRedirectURIs: []string{"http://127.0.0.1:*", "http://[::1]:*"}

7.2 Token Management Failures

Problem: Access token expiration disrupts MCP sessions.

Solution:

  • Implement automatic token refresh
  • Attach refresh tokens to MCP context objects
  • Use short-lived tokens (under 10 minutes)

Go Implementation:

go
func RefreshToken(client *mcp.Client) error {
  newToken, err := client.OAuth.RefreshToken()
  client.SetAccessToken(newToken)
  return err
}

7.3 "Connected but Token Expired" Status

Problem: mcpproxy auth status shows server as "Authenticated & Connected" but token shows "⚠️ EXPIRED".

Explanation: This is expected behavior, not a bug:

  • Connection persists after token expiration: Once an MCP connection is established, it remains active. HTTP connections don't automatically drop when tokens expire.
  • Token refresh happens in-memory: mcp-go's OAuth handler automatically refreshes tokens during API calls. The refreshed token lives in mcp-go's memory.
  • Status displays storage value: auth status reads the original expiration from persistent storage (BBolt), which may lag behind in-memory state.

When this occurs:

  • Short-lived tokens (e.g., 30-second TTL) expire quickly after storage
  • Server connected successfully, then stored token expired
  • mcp-go refreshed the token automatically OR the connection is still active from before expiration

How to verify: Call a tool on the server. If it works, mcp-go is refreshing tokens automatically.

Contrast with "Disconnected + Expired": If status shows "Disconnected" with "EXPIRED" token, the refresh token also failed (expired or missing), requiring re-authentication via mcpproxy auth login.

7.4 PKCE Implementation Gaps

Problem: Code interception via port sniffing.

Solution:

  • Enforce S256 PKCE method
  • Bind code_verifier to client session
  • Reject plain code_challenge methods

MCP Spec Requirement: MCP servers must require PKCE for public clients.

8. Critical Issue: Redirect URI Exact Matching - SOLVED ✅

8.1 The Problem and Our Solution

RFC 8252 vs Implementation Reality: While RFC 8252 states that authorization servers "MUST allow any port to be specified at the time of the request for loopback IP redirect URIs", many OAuth providers including Cloudflare implement exact URI matching for security reasons.

MCPProxy's Solution: We've implemented a comprehensive Callback Server Coordination System that solves this exact matching requirement while maintaining RFC 8252 compliance.

8.2 MCPProxy Implementation Architecture

MCPProxy successfully handles Cloudflare's strict validation through:

  • Global Callback Server Manager: Coordinates OAuth callback servers across all upstream connections
  • Dynamic Port Allocation: Each OAuth flow gets a unique, OS-assigned port
  • Perfect URI Consistency: Exact match between Dynamic Client Registration and OAuth redirect_uri parameters
  • Lifecycle Management: Proper startup, shutdown, and cleanup of callback servers

8.3 Implementation Details

1. Callback Server Manager

go
type CallbackServerManager struct {
    servers map[string]*CallbackServer
    mu      sync.RWMutex
    logger  *zap.Logger
}

func (m *CallbackServerManager) StartCallbackServer(serverName string) (*CallbackServer, error) {
    // Allocate dynamic port
    listener, err := net.Listen("tcp", "127.0.0.1:0")
    if err != nil {
        return nil, fmt.Errorf("failed to allocate dynamic port: %w", err)
    }
    
    // Extract port and create redirect URI
    addr := listener.Addr().(*net.TCPAddr)
    port := addr.Port
    redirectURI := fmt.Sprintf("http://127.0.0.1:%d/oauth/callback", port)
    
    // Create dedicated HTTP server for this callback
    mux := http.NewServeMux()
    server := &http.Server{
        Addr:    fmt.Sprintf("127.0.0.1:%d", port),
        Handler: mux,
    }
    
    // Start server with proper callback handling
    callbackServer := &CallbackServer{
        Port:         port,
        RedirectURI:  redirectURI,
        Server:       server,
        CallbackChan: make(chan map[string]string, 1),
        logger:       m.logger.With(zap.String("server", serverName)),
    }
    
    // Set up callback handler
    mux.HandleFunc("/oauth/callback", callbackServer.handleCallback)
    
    // Start server on the allocated port
    go server.Serve(listener)
    
    return callbackServer, nil
}

2. OAuth Configuration with Dynamic URI

go
func CreateOAuthConfig(serverConfig *config.ServerConfig) *client.OAuthConfig {
    // Start callback server first to get the exact port
    callbackServer, err := globalCallbackManager.StartCallbackServer(serverConfig.Name)
    if err != nil {
        logger.Error("Failed to start OAuth callback server", zap.Error(err))
        return nil
    }
    
    // Use the exact redirect URI in OAuth config
    return &client.OAuthConfig{
        ClientID:              "",                         // Dynamic Client Registration
        ClientSecret:          "",                         // PKCE flow
        RedirectURI:           callbackServer.RedirectURI, // Exact URI with allocated port
        Scopes:                []string{"mcp.read", "mcp.write"},
        PKCEEnabled:           true,
        AuthServerMetadataURL: buildMetadataURL(serverConfig.URL),
    }
}

3. Coordinated OAuth Flow

go
// In upstream client initialization
func (c *Client) handleOAuthFlow(oauthHandler *client.OAuthHandler) error {
    // Step 1: Dynamic Client Registration with exact URI
    if err := oauthHandler.RegisterClient(ctx, "mcpproxy-go"); err != nil {
        return fmt.Errorf("DCR failed: %w", err)
    }
    
    // Step 2: Generate PKCE and state parameters
    codeVerifier, _ := client.GenerateCodeVerifier()
    codeChallenge := client.GenerateCodeChallenge(codeVerifier)
    state, _ := client.GenerateState()
    
    // Step 3: Get authorization URL (uses exact redirect URI from DCR)
    authURL, _ := oauthHandler.GetAuthorizationURL(ctx, state, codeChallenge)
    
    // Step 4: Open browser and wait for callback
    openBrowser(authURL)
    
    callbackServer, _ := oauth.GetGlobalCallbackManager().GetCallbackServer(c.config.Name)
    
    // Step 5: Wait for authorization code on our callback server
    select {
    case authParams := <-callbackServer.CallbackChan:
        // Step 6: Validate state and exchange code for tokens
        if authParams["state"] != state {
            return fmt.Errorf("OAuth state mismatch")
        }
        
        return oauthHandler.ProcessAuthorizationResponse(ctx, 
            authParams["code"], state, codeVerifier)
    case <-time.After(5 * time.Minute):
        return fmt.Errorf("OAuth authorization timeout")
    }
}

8.4 Key Success Factors

For Cloudflare MCP servers and other strict OAuth providers:

  • Never use wildcard redirect URIs in DCR registration
  • Always include the exact port in redirect_uris array
  • Ensure perfect matching between DCR and OAuth redirect_uri parameters
  • Start callback server before DCR to know the port
  • Use dedicated HTTP server per OAuth flow to avoid conflicts
  • Implement proper lifecycle management for callback servers

8.5 Production Results

MCPProxy's implementation successfully handles:

  • Cloudflare AutoRAG OAuth: Exact URI matching with dynamic ports
  • Multiple Concurrent OAuth Flows: Each server gets its own callback server
  • Zero Port Conflicts: OS-assigned ephemeral ports prevent collisions
  • Automatic Retry: Post-OAuth MCP initialization retry ensures seamless connection
  • RFC 8252 Compliance: Uses 127.0.0.1 loopback with PKCE security

Example Log Output:

2025-07-13T09:30:07.119 | INFO | OAuth callback server started successfully | 
  {"server": "cloudflare_autorag", "redirect_uri": "http://127.0.0.1:64020/oauth/callback", "port": 64020}
2025-07-13T09:30:07.119 | INFO | Opening browser for OAuth authentication | 
  {"auth_url": "https://autorag.mcp.cloudflare.com/oauth/authorize?...&redirect_uri=http%3A%2F%2F127.0.0.1%3A64020%2Foauth%2Fcallback..."}
2025-07-13T09:30:56.674 | INFO | OAuth callback received | 
  {"params": {"code": "...", "state": "..."}}
2025-07-13T09:30:57.507 | INFO | OAuth authentication completed successfully

9. Conclusion and Recommendations

The integration of OAuth 2.1 with MCP servers in Go requires strict adherence to IETF standards, particularly RFC 8252 for native apps and RFC 7591 for dynamic registration. Our analysis demonstrates that libraries like mcp-golang provide robust foundations, and MCPProxy has successfully implemented a production-ready solution that addresses all critical challenges:

Successfully Implemented Solutions

  1. Redirect URI Exact Matching: SOLVED through our Global Callback Server Manager with dynamic port allocation and perfect URI consistency
  2. RFC 8252 Compliance: ACHIEVED with 127.0.0.1 loopback interface and OS-assigned ephemeral ports
  3. PKCE Security: IMPLEMENTED with mandatory PKCE-S256 for all OAuth flows
  4. Dynamic Client Registration: WORKING seamlessly with Cloudflare and other OAuth providers
  5. Token Management: AUTOMATED with refresh token handling and secure storage

🚀 MCPProxy Implementation Highlights

For Cloudflare MCP OAuth (and other strict providers):

  • Perfect URI matching with callback server coordination
  • Zero port conflicts through dedicated servers per OAuth flow
  • Automatic retry post-OAuth for seamless MCP connection
  • Production-tested with Cloudflare AutoRAG OAuth flows
  • RFC 8252 compliant with enhanced security

📋 Best Practices Proven in Production

Based on MCPProxy's successful implementation:

  1. Always use callback server coordination instead of simple port allocation
  2. Start callback servers before Dynamic Client Registration to ensure exact URI availability
  3. Implement dedicated HTTP servers per OAuth flow to prevent race conditions
  4. Use OS-assigned ephemeral ports with proper lifecycle management
  5. Coordinate OAuth flow with MCP client initialization for seamless user experience
  6. Implement comprehensive logging for OAuth flow debugging and monitoring

🔮 Future Enhancements

MCPProxy's architecture enables future enhancements:

  • Persistent token storage for cross-session authentication persistence
  • OAuth server health monitoring with automatic token refresh
  • Multi-tenant OAuth support for enterprise deployments
  • OAuth scope management with fine-grained permission control
  • OAuth metrics and analytics for usage monitoring

📖 Reference Implementation

MCPProxy serves as a reference implementation for OAuth 2.1 with MCP servers, demonstrating:

  • How to solve the critical redirect URI exact matching challenge
  • Production-ready callback server coordination patterns
  • Seamless integration with the mcp-go library's OAuth capabilities
  • RFC 8252 compliance in real-world deployment scenarios

The patterns documented and implemented in MCPProxy establish secure, scalable MCP-OAuth integrations for Go-based AI agent ecosystems, successfully balancing user consent with operational security while meeting the strict requirements of modern OAuth providers like Cloudflare.