Skip to content

004. OAuth Proxy for Remote MCP Servers

Context

We have added support for connecting to remote MCP servers (e.g., mcp-kubernetes). These servers often require authentication, specifically OAuth 2.1 (using mcp-oauth).

The muster architecture consists of: - muster agent: Runs locally (e.g., on a user's laptop), integrated with clients like Cursor via stdio. - muster server: Runs centrally (e.g., on a management cluster), aggregating tools from remote MCP servers.

We need a flow where: 1. The User interacts with Cursor (Agent). 2. The Agent calls the Server. 3. The Server calls the Remote MCP. 4. If the Remote MCP requires authentication, the User can authenticate via their browser. 5. Cursor/Agent should not receive the sensitive token; the Server should manage it and proxy the authenticated requests.

Decision

We will implement an OAuth Proxy pattern where the muster server acts as the OAuth Client on behalf of the user.

1. Roles

  • muster server (OAuth Client & Proxy):

    • Maintains OAuth configurations for downstream Remote MCP servers.
    • Acts as the registered OAuth Client (using CIMD).
    • Initiates the Authorization Code flow.
    • Hosts the Redirect URI endpoint (/oauth/proxy/callback) to receive authorization codes.
    • Exchanges codes for Access/Refresh tokens.
    • Stores tokens securely, associated with the user's session.
    • Injects the Access Token into outgoing requests to the Remote MCP.
  • muster agent (UI Bridge):

    • Detects "Authentication Required" responses from the muster server.
    • Presents the Authorization URL to the user via the Tool Result (as text/link).
    • Instruction: "Please authenticate in your browser: [Link]".

2. Authentication Flow

  1. Tool Call: User requests an action (e.g., "List Pods"). Cursor calls muster agent, which forwards the call to muster server.
  2. Auth Check: muster server attempts to call the Remote MCP (mcp-kubernetes).
    • If the request fails with 401 Unauthorized or if no token exists for this session:
    • The Server generates an OAuth Authorization URL for the Remote MCP.
    • The Server generates a unique state parameter to link the flow.
  3. Auth Challenge: muster server returns a structured "Auth Required" error/response to the muster agent.
    • Payload: { "status": "auth_required", "auth_url": "https://..." }
  4. User Interaction: muster agent formats this into a user-friendly message for Cursor.
    • Example Tool Output: Authentication required for Kubernetes. Please visit: https://remote-mcp/oauth/authorize?...
  5. Browser Flow:
    • User clicks the link.
    • User authenticates with the Identity Provider (e.g., Dex/Google).
    • Browser redirects to muster server's callback URL: https://muster.example.com/oauth/proxy/callback?code=...&state=....
  6. Token Exchange:
    • muster server receives the code.
    • Exchanges it for an Access Token (and Refresh Token).
    • Stores the token in memory (or persistent store) mapped to the user's session.
    • Displays an HTML success page: "Authentication Successful. You may return to Cursor."
  7. Retry:
    • User sees the success message.
    • User retries the instruction in Cursor (e.g., "Try again").
    • muster server now finds the valid token and proxies the request successfully.

3. Session Management

To link the Tool Call (Step 1) with the Callback (Step 6), we need a session identifier. * The muster agent should generate a persistent session_id (e.g., UUID) on startup. * This session_id is sent with every request to muster server (e.g., in a Header X-muster-Session-ID). * The muster server uses this ID to store and retrieve tokens.

4. Client Registration (Self-Hosted CIMD)

muster serves its own Client ID Metadata Document (CIMD) dynamically, eliminating the need for external static hosting.

  • Self-Hosted: When oauth.publicUrl is set, muster auto-derives the client ID as {publicUrl}/.well-known/oauth-client.json and serves the CIMD at that path.
  • Client ID: The CIMD URL is used as the client_id when authenticating with remote MCP servers.
  • Content: The JSON document defines the client's properties with the correct redirect URI for the deployment:
    {
      "client_id": "https://muster.example.com/.well-known/oauth-client.json",
      "client_name": "muster MCP Aggregator",
      "redirect_uris": ["https://muster.example.com/oauth/proxy/callback"],
      "grant_types": ["authorization_code", "refresh_token"],
      "response_types": ["code"],
      "token_endpoint_auth_method": "none"
    }
    
  • No External Hosting Required: Each deployment automatically gets the correct redirect URI without needing to maintain external CIMD files.

5. Single Sign-On (SSO) Mechanisms

muster supports two SSO mechanisms for downstream MCP servers:

  • Token Forwarding: When muster itself is protected by OAuth, it can forward its ID token to downstream servers that trust muster's OAuth client ID. Configure with auth.forwardToken: true in MCPServer spec.
  • Token Exchange (RFC 8693): For cross-cluster SSO where clusters have separate IdPs, muster can exchange its local token for one valid on the remote cluster's IdP. Configure with auth.tokenExchange in MCPServer spec.

Consequences

  • Public Reachability: muster server requires a public URL (Ingress) to receive OAuth callbacks.
  • Stateful Server: The server needs to manage user sessions and tokens. For HA, a distributed store (e.g., Redis/Valkey) might be needed in the future, but in-memory is sufficient for the initial MVP (single replica).
  • User Experience: The user must manually click a link and then retry the action in Cursor. This is a limitation of the decoupled architecture but provides high security (token never leaves the server boundary). With SSO via Token Forwarding or Token Exchange, subsequent auths become transparent.

Implementation Steps

  1. muster server:
    • Add internal/oauth/client package.
    • Implement /oauth/proxy/callback handler.
    • Implement /.well-known/oauth-client.json handler for self-hosted CIMD.
    • Add Session/Token Store (In-Memory).
    • Update aggregator to intercept 401s and trigger flow.
  2. muster agent:
    • Update agent to handle "Auth Required" responses and format them for Cursor.
  3. Configuration:
    • Add flags for public-url (for callback construction and CIMD generation).
    • Add optional flag for client-id (only needed for external CIMD hosting).

Implementation Notes

The following additions and adaptations were made during implementation that differ from or extend the original design:

1. PKCE (Proof Key for Code Exchange)

The implementation uses PKCE with S256 code challenge method for enhanced security. This was not explicitly mentioned in the original design but is required for OAuth 2.1 compliance with public clients.

  • A cryptographic code verifier (32 random bytes, base64url-encoded) is generated for each auth flow
  • The S256 challenge (SHA256 hash of verifier) is sent in the authorization request
  • The code verifier is stored server-side with the state (never transmitted to browser)
  • During token exchange, the code verifier is sent to prove the same client initiated the flow

2. Self-Hosted CIMD (Dynamic Client Metadata)

muster serves its own CIMD dynamically:

  • When oauth.publicUrl is set, muster auto-derives the client ID as {publicUrl}/.well-known/oauth-client.json
  • muster serves the CIMD at this path with dynamically generated content matching the deployment's actual redirect URI
  • This eliminates the need to maintain external CIMD files

Configuration:

aggregator:
  oauth:
    enabled: true
    publicUrl: "https://muster.example.com"
    # clientId is auto-derived from publicUrl

3. OAuth Metadata Discovery with Fallback

The implementation tries multiple discovery endpoints:

  1. RFC 8414: {issuer}/.well-known/oauth-authorization-server
  2. OpenID Connect: {issuer}/.well-known/openid-configuration

This provides compatibility with both pure OAuth 2.0 authorization servers and OpenID Connect providers.

4. Metadata Caching with Deduplication

  • OAuth metadata is cached for 30 minutes (TTL-based)
  • singleflight package prevents duplicate concurrent fetches for the same issuer
  • Cache is validated on each access; expired entries trigger re-fetch

5. Background Cleanup Goroutines

Both stores run background cleanup loops that must be stopped to prevent leaks:

  • TokenStore: Cleans expired tokens every 5 minutes
  • StateStore: Cleans expired states every 1 minute (states expire after 10 minutes)
  • Both expose Stop() methods called during graceful shutdown

6. Enhanced State Management

The state parameter implementation includes:

  • Encoded state (transmitted via URL): Contains session ID, server name, issuer, nonce, creation timestamp
  • Server-side state (indexed by nonce): Also stores the PKCE code verifier (excluded from URL via json:"-")
  • States are consumed on validation (one-time use) to prevent replay attacks
  • 10-minute expiration prevents stale auth flows

7. Security Hardening

Several security measures were added during implementation:

  • HTTP Security Headers: Success/error pages include X-Content-Type-Options: nosniff, X-Frame-Options: DENY, CSP, Referrer-Policy: no-referrer, Cache-Control: no-store
  • Session ID Truncation: Only first 8 characters logged to prevent session hijacking via log analysis
  • XSS Prevention: Server names and error messages are HTML-escaped before rendering
  • Token Expiry Margin: 30-second margin when checking token validity accounts for clock skew and network latency
  • Generic Error Messages: OAuth errors shown to users are generic; detailed errors only appear in server logs

8. Styled HTML Templates

Success and error pages use embedded HTML templates (go:embed) with:

  • Modern styling with gradient backgrounds
  • Clear visual indicators (checkmark for success, X for error)
  • Mobile-responsive design
  • Consistent branding ("Powered by muster")

9. Extended CIMD Schema

The served CIMD includes additional fields beyond the original design:

{
  "client_id": "https://muster.example.com/.well-known/oauth-client.json",
  "client_name": "muster MCP Aggregator",
  "client_uri": "https://github.com/giantswarm/muster",
  "redirect_uris": ["https://muster.example.com/oauth/proxy/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "openid profile email",
  "software_id": "giantswarm-muster",
  "software_version": "1.0.0"
}

10. API Adapter Pattern

The implementation follows the project's service locator pattern:

  • internal/oauth package implements all OAuth logic
  • api_adapter.go wraps the Manager and implements api.OAuthHandler interface
  • Other packages (e.g., aggregator) access OAuth functionality via api.GetOAuthHandler()
  • This maintains decoupling and enables testing with mock handlers

11. Token Refresh Support

Full token refresh implementation:

  • RefreshToken() method handles refresh token grant
  • Logs refresh operations at INFO level with duration metrics
  • Preserves refresh token if not returned in refresh response
  • New tokens automatically update expiration timestamps

12. SSO Token Lookup

Two-tier token lookup for SSO support:

  1. Exact match: (SessionID, Issuer, Scope)
  2. Issuer-only fallback: (SessionID, Issuer) - enables reuse across different scopes