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¶
- Tool Call: User requests an action (e.g., "List Pods"). Cursor calls
muster agent, which forwards the call tomuster server. - Auth Check:
muster serverattempts to call the Remote MCP (mcp-kubernetes).- If the request fails with
401 Unauthorizedor if no token exists for this session: - The Server generates an OAuth Authorization URL for the Remote MCP.
- The Server generates a unique
stateparameter to link the flow.
- If the request fails with
- Auth Challenge:
muster serverreturns a structured "Auth Required" error/response to themuster agent.- Payload:
{ "status": "auth_required", "auth_url": "https://..." }
- Payload:
- User Interaction:
muster agentformats this into a user-friendly message for Cursor.- Example Tool Output:
Authentication required for Kubernetes. Please visit: https://remote-mcp/oauth/authorize?...
- Example Tool Output:
- 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=....
- Token Exchange:
muster serverreceives 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."
- Retry:
- User sees the success message.
- User retries the instruction in Cursor (e.g., "Try again").
muster servernow 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.publicUrlis set, muster auto-derives the client ID as{publicUrl}/.well-known/oauth-client.jsonand serves the CIMD at that path. - Client ID: The CIMD URL is used as the
client_idwhen 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: truein 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.tokenExchangein MCPServer spec.
Consequences¶
- Public Reachability:
muster serverrequires 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¶
- muster server:
- Add
internal/oauth/clientpackage. - Implement
/oauth/proxy/callbackhandler. - Implement
/.well-known/oauth-client.jsonhandler for self-hosted CIMD. - Add Session/Token Store (In-Memory).
- Update
aggregatorto intercept 401s and trigger flow.
- Add
- muster agent:
- Update
agentto handle "Auth Required" responses and format them for Cursor.
- Update
- Configuration:
- Add flags for
public-url(for callback construction and CIMD generation). - Add optional flag for
client-id(only needed for external CIMD hosting).
- Add flags for
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.publicUrlis 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:
- RFC 8414:
{issuer}/.well-known/oauth-authorization-server - 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)
singleflightpackage 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/oauthpackage implements all OAuth logicapi_adapter.gowraps the Manager and implementsapi.OAuthHandlerinterface- 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:
- Exact match:
(SessionID, Issuer, Scope) - Issuer-only fallback:
(SessionID, Issuer)- enables reuse across different scopes