Skip to content

Commit a27d766

Browse files
committed
fix ci
1 parent 969ebc2 commit a27d766

File tree

5 files changed

+387
-298
lines changed

5 files changed

+387
-298
lines changed

pkg/vmcp/client/client.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
"github.com/stacklok/toolhive/pkg/logger"
1919
"github.com/stacklok/toolhive/pkg/vmcp"
20-
"github.com/stacklok/toolhive/pkg/vmcp/auth"
20+
vmcpauth "github.com/stacklok/toolhive/pkg/vmcp/auth"
2121
)
2222

2323
const (
@@ -48,7 +48,7 @@ type httpBackendClient struct {
4848

4949
// registry manages authentication strategies for outgoing requests to backend MCP servers.
5050
// Must not be nil - use UnauthenticatedStrategy for no authentication.
51-
registry auth.OutgoingAuthRegistry
51+
registry vmcpauth.OutgoingAuthRegistry
5252
}
5353

5454
// NewHTTPBackendClient creates a new HTTP-based backend client.
@@ -59,7 +59,7 @@ type httpBackendClient struct {
5959
// "unauthenticated" strategy.
6060
//
6161
// Returns an error if registry is nil.
62-
func NewHTTPBackendClient(registry auth.OutgoingAuthRegistry) (vmcp.BackendClient, error) {
62+
func NewHTTPBackendClient(registry vmcpauth.OutgoingAuthRegistry) (vmcp.BackendClient, error) {
6363
if registry == nil {
6464
return nil, fmt.Errorf("registry cannot be nil; use UnauthenticatedStrategy for no authentication")
6565
}
@@ -79,12 +79,29 @@ func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
7979
return f(req)
8080
}
8181

82+
// contextPropagatingRoundTripper propagates the original request context to HTTP requests.
83+
// This ensures that identity information from the vMCP handler is available for authentication.
84+
type contextPropagatingRoundTripper struct {
85+
base http.RoundTripper
86+
origCtx context.Context
87+
}
88+
89+
// RoundTrip implements http.RoundTripper by propagating the original context to the request.
90+
func (c *contextPropagatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
91+
// Clone the request with the original context that contains the identity
92+
reqWithCtx := req.Clone(c.origCtx)
93+
// Copy headers from the original request
94+
reqWithCtx.Header = req.Header
95+
96+
return c.base.RoundTrip(reqWithCtx)
97+
}
98+
8299
// authRoundTripper is an http.RoundTripper that adds authentication to backend requests.
83100
// The authentication strategy and metadata are pre-resolved and validated at client creation time,
84101
// eliminating per-request lookups and validation overhead.
85102
type authRoundTripper struct {
86103
base http.RoundTripper
87-
authStrategy auth.Strategy
104+
authStrategy vmcpauth.Strategy
88105
authMetadata map[string]any
89106
target *vmcp.BackendTarget
90107
}
@@ -110,7 +127,7 @@ func (a *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
110127
// It handles defaulting to "unauthenticated" when no strategy is specified.
111128
// This method should be called once at client creation time to enable fail-fast
112129
// behavior for invalid authentication configurations.
113-
func (h *httpBackendClient) resolveAuthStrategy(target *vmcp.BackendTarget) (auth.Strategy, error) {
130+
func (h *httpBackendClient) resolveAuthStrategy(target *vmcp.BackendTarget) (vmcpauth.Strategy, error) {
114131
strategyName := target.AuthStrategy
115132

116133
// Default to unauthenticated if not specified
@@ -129,7 +146,7 @@ func (h *httpBackendClient) resolveAuthStrategy(target *vmcp.BackendTarget) (aut
129146

130147
// defaultClientFactory creates mark3labs MCP clients for different transport types.
131148
func (h *httpBackendClient) defaultClientFactory(ctx context.Context, target *vmcp.BackendTarget) (*client.Client, error) {
132-
// Build transport chain: size limit → authentication → HTTP
149+
// Build transport chain: size limit → context propagation → authentication → HTTP
133150
var baseTransport = http.DefaultTransport
134151

135152
// Resolve authentication strategy ONCE at client creation time
@@ -153,6 +170,14 @@ func (h *httpBackendClient) defaultClientFactory(ctx context.Context, target *vm
153170
target: target,
154171
}
155172

173+
// Add context propagation layer to ensure identity is available for authentication
174+
// This must come AFTER authentication layer so auth round tripper gets the propagated context
175+
// We use the same context passed to this function, which should have the identity
176+
baseTransport = &contextPropagatingRoundTripper{
177+
base: baseTransport,
178+
origCtx: ctx,
179+
}
180+
156181
// Add size limit layer for DoS protection
157182
sizeLimitedTransport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
158183
resp, err := baseTransport.RoundTrip(req)

pkg/vmcp/types.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,6 @@ type BackendTarget struct {
5858
//
5959
// When a backend MCPServer has OIDCConfig configured, it means clients (including vMCP)
6060
// must present OIDC tokens to access that backend.
61-
//
62-
// Discovery Mode: When vMCP's outgoing auth mode is "discovered", vMCP will use the
63-
// authentication configuration defined in the backend MCPServer. This field stores the
64-
// discovered OIDC config from the backend's OIDCConfig spec, which vMCP uses to
65-
// authenticate when accessing OIDC-protected backends.
66-
//
67-
// See pkg/vmcp/workloads/k8s.go:discoverIncomingOIDCConfig for discovery implementation.
6861
IncomingOIDCConfig map[string]interface{}
6962

7063
// SessionAffinity indicates if requests from the same session
@@ -151,13 +144,6 @@ type Backend struct {
151144
//
152145
// When a backend MCPServer has OIDCConfig configured, it means clients (including vMCP)
153146
// must present OIDC tokens to access that backend.
154-
//
155-
// Discovery Mode: When vMCP's outgoing auth mode is "discovered", vMCP will use the
156-
// authentication configuration defined in the backend MCPServer. This field stores the
157-
// discovered OIDC config from the backend's OIDCConfig spec, which vMCP uses to
158-
// authenticate when accessing OIDC-protected backends.
159-
//
160-
// See pkg/vmcp/workloads/k8s.go:discoverIncomingOIDCConfig for discovery implementation.
161147
IncomingOIDCConfig map[string]interface{}
162148

163149
// Metadata stores additional backend information.

pkg/vmcp/workloads/k8s.go

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ import (
55
"fmt"
66
"strings"
77

8-
corev1 "k8s.io/api/core/v1"
98
"k8s.io/apimachinery/pkg/api/errors"
109
"k8s.io/apimachinery/pkg/runtime"
11-
"k8s.io/apimachinery/pkg/types"
1210
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
1311
"sigs.k8s.io/controller-runtime/pkg/client"
1412

@@ -200,12 +198,6 @@ func (d *k8sDiscoverer) mcpServerToBackend(ctx context.Context, mcpServer *mcpv1
200198
return nil
201199
}
202200

203-
// Discover and populate incoming OIDC configuration from MCPServer
204-
if err := d.discoverIncomingOIDCConfig(ctx, mcpServer, backend); err != nil {
205-
logger.Errorf("Failed to discover incoming OIDC config for MCPServer %s: %v", mcpServer.Name, err)
206-
return nil
207-
}
208-
209201
return backend
210202
}
211203

@@ -242,97 +234,6 @@ func (d *k8sDiscoverer) discoverAuthConfig(ctx context.Context, mcpServer *mcpv1
242234
return nil
243235
}
244236

245-
// discoverIncomingOIDCConfig discovers and stores the backend's OIDC authentication requirements.
246-
//
247-
// When a backend MCPServer has OIDCConfig configured, it means clients (including vMCP) must present
248-
// OIDC tokens to access that backend. This method discovers the backend's OIDC configuration and
249-
// stores it in backend.IncomingOIDCConfig.
250-
//
251-
// Authentication Flow:
252-
// - When vMCP's outgoing auth mode is "discovered", vMCP will use the authentication configuration
253-
// defined in the backend MCPServer (via ExternalAuthConfigRef for token exchange/header injection,
254-
// or via OIDCConfig for OIDC-protected backends)
255-
// - The discovered OIDC config is stored in Backend.IncomingOIDCConfig (see pkg/vmcp/types.go)
256-
// - This config is used by vMCP to authenticate to backends that require OIDC tokens
257-
//
258-
// Return behavior:
259-
// - Returns nil error if OIDCConfig is nil (no OIDC required) - this is expected behavior
260-
// - Returns nil error if OIDC config is discovered and successfully populated into backend
261-
// - Returns error if OIDC config exists but discovery/resolution fails (e.g., secret not found)
262-
func (d *k8sDiscoverer) discoverIncomingOIDCConfig(
263-
ctx context.Context, mcpServer *mcpv1alpha1.MCPServer, backend *vmcp.Backend,
264-
) error {
265-
// If no OIDC config, nothing to discover
266-
if mcpServer.Spec.OIDCConfig == nil {
267-
logger.Debugf("MCPServer %s has no OIDCConfig, no incoming auth required", mcpServer.Name)
268-
return nil
269-
}
270-
271-
oidcConfig := mcpServer.Spec.OIDCConfig
272-
273-
// Convert OIDC config to map for storage in backend
274-
config := make(map[string]interface{})
275-
276-
// Handle inline OIDC configuration
277-
if oidcConfig.Type == "inline" && oidcConfig.Inline != nil {
278-
inline := oidcConfig.Inline
279-
config["issuer"] = inline.Issuer
280-
281-
if inline.Audience != "" {
282-
config["audience"] = inline.Audience
283-
}
284-
285-
if inline.ClientID != "" {
286-
config["client_id"] = inline.ClientID
287-
}
288-
289-
// Resolve client secret from secret reference if present
290-
if inline.ClientSecretRef != nil {
291-
secret := &corev1.Secret{}
292-
secretKey := types.NamespacedName{
293-
Name: inline.ClientSecretRef.Name,
294-
Namespace: mcpServer.Namespace,
295-
}
296-
297-
if err := d.k8sClient.Get(ctx, secretKey, secret); err != nil {
298-
return fmt.Errorf("failed to get secret %s/%s: %w",
299-
mcpServer.Namespace, inline.ClientSecretRef.Name, err)
300-
}
301-
302-
secretValue, ok := secret.Data[inline.ClientSecretRef.Key]
303-
if !ok {
304-
return fmt.Errorf("secret %s/%s does not contain key %s",
305-
mcpServer.Namespace, inline.ClientSecretRef.Name, inline.ClientSecretRef.Key)
306-
}
307-
308-
config["client_secret"] = string(secretValue)
309-
} else if inline.ClientSecret != "" {
310-
// Use direct client secret if provided (not recommended but supported)
311-
config["client_secret"] = inline.ClientSecret
312-
}
313-
314-
if inline.JWKSURL != "" {
315-
config["jwks_url"] = inline.JWKSURL
316-
}
317-
318-
if inline.IntrospectionURL != "" {
319-
config["introspection_url"] = inline.IntrospectionURL
320-
}
321-
322-
// Add security flags
323-
config["insecure_allow_http"] = inline.InsecureAllowHTTP
324-
config["jwks_allow_private_ip"] = inline.JWKSAllowPrivateIP
325-
config["protected_resource_allow_private_ip"] = inline.ProtectedResourceAllowPrivateIP
326-
}
327-
328-
// Store the discovered OIDC config
329-
backend.IncomingOIDCConfig = config
330-
331-
logger.Infof("✓ Discovered incoming OIDC config for MCPServer %s: issuer=%s, client_id=%s",
332-
mcpServer.Name, config["issuer"], config["client_id"])
333-
return nil
334-
}
335-
336237
// mapK8SWorkloadPhaseToHealth converts a MCPServerPhase to a backend health status.
337238
func mapK8SWorkloadPhaseToHealth(phase mcpv1alpha1.MCPServerPhase) vmcp.BackendHealthStatus {
338239
switch phase {

0 commit comments

Comments
 (0)