Skip to content

Commit 969ebc2

Browse files
committed
fixes from review
1 parent 52ff189 commit 969ebc2

File tree

4 files changed

+116
-48
lines changed

4 files changed

+116
-48
lines changed

pkg/vmcp/types.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,17 @@ type BackendTarget struct {
5454
// This is opaque to the router and interpreted by the authenticator.
5555
AuthMetadata map[string]any
5656

57-
// IncomingOIDCConfig contains the discovered OIDC configuration for vMCP→backend authentication.
58-
// When a backend MCPServer has OIDCConfig configured, vMCP must authenticate to that backend
59-
// using OIDC. This field stores the discovered OIDC configuration.
57+
// IncomingOIDCConfig contains the backend's OIDC authentication requirements.
58+
//
59+
// When a backend MCPServer has OIDCConfig configured, it means clients (including vMCP)
60+
// 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.
6068
IncomingOIDCConfig map[string]interface{}
6169

6270
// SessionAffinity indicates if requests from the same session
@@ -139,9 +147,17 @@ type Backend struct {
139147
// AuthMetadata contains strategy-specific auth configuration.
140148
AuthMetadata map[string]any
141149

142-
// IncomingOIDCConfig contains the discovered OIDC configuration for vMCP→backend authentication.
143-
// When a backend MCPServer has OIDCConfig configured, vMCP must authenticate to that backend
144-
// using OIDC. This field stores the discovered OIDC configuration.
150+
// IncomingOIDCConfig contains the backend's OIDC authentication requirements.
151+
//
152+
// When a backend MCPServer has OIDCConfig configured, it means clients (including vMCP)
153+
// 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.
145161
IncomingOIDCConfig map[string]interface{}
146162

147163
// Metadata stores additional backend information.

pkg/vmcp/workloads/k8s.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,15 +242,23 @@ func (d *k8sDiscoverer) discoverAuthConfig(ctx context.Context, mcpServer *mcpv1
242242
return nil
243243
}
244244

245-
// discoverIncomingOIDCConfig discovers and populates incoming OIDC configuration from the MCPServer's OIDCConfig.
246-
// When a backend MCPServer has OIDCConfig configured, it means clients (including vMCP) must authenticate
247-
// using OIDC to access that backend. This method discovers and converts the OIDC configuration so vMCP
248-
// can authenticate to the backend.
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
249257
//
250258
// Return behavior:
251259
// - Returns nil error if OIDCConfig is nil (no OIDC required) - this is expected behavior
252260
// - Returns nil error if OIDC config is discovered and successfully populated into backend
253-
// - Returns error if OIDC config exists but discovery/resolution fails
261+
// - Returns error if OIDC config exists but discovery/resolution fails (e.g., secret not found)
254262
func (d *k8sDiscoverer) discoverIncomingOIDCConfig(
255263
ctx context.Context, mcpServer *mcpv1alpha1.MCPServer, backend *vmcp.Backend,
256264
) error {

test/e2e/thv-operator/virtualmcp/helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ func GetPodLogsForDeployment(ctx context.Context, c client.Client, namespace, de
435435
}
436436

437437
// GetPodLogs returns logs from a specific pod and container
438-
func GetPodLogs(ctx context.Context, _ client.Client, podName, namespace, containerName string) (string, error) {
438+
func GetPodLogs(ctx context.Context, podName, namespace, containerName string) (string, error) {
439439
logs, err := getPodLogs(ctx, namespace, podName, containerName, false)
440440
if err != nil {
441441
return "", fmt.Errorf("failed to get logs for pod %s container %s: %w", podName, containerName, err)

test/e2e/thv-operator/virtualmcp/virtualmcp_auth_discovery_test.go

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,20 @@ var _ = Describe("VirtualMCPServer Auth Discovery", Ordered, func() {
2828
)
2929

3030
var (
31-
testNamespace = "default"
32-
mcpGroupName = "test-auth-discovery-group"
33-
vmcpServerName = "test-vmcp-auth-discovery"
34-
backend1Name = "backend-with-token-exchange"
35-
backend2Name = "backend-with-header-injection"
36-
backend3Name = "backend-no-auth"
37-
authConfig1Name = "test-token-exchange-auth"
38-
authConfig2Name = "test-header-injection-auth"
39-
authSecret1Name = "test-token-exchange-secret"
40-
authSecret2Name = "test-header-injection-secret"
41-
timeout = 5 * time.Minute
42-
pollingInterval = 5 * time.Second
43-
mockServer *httptest.Server
31+
testNamespace = "default"
32+
mcpGroupName = "test-auth-discovery-group"
33+
vmcpServerName = "test-vmcp-auth-discovery"
34+
backend1Name = "backend-with-token-exchange"
35+
backend2Name = "backend-with-header-injection"
36+
backend3Name = "backend-no-auth"
37+
authConfig1Name = "test-token-exchange-auth"
38+
authConfig2Name = "test-header-injection-auth"
39+
authSecret1Name = "test-token-exchange-secret"
40+
authSecret2Name = "test-header-injection-secret"
41+
oidcClientSecretName = "test-oidc-client-secret"
42+
timeout = 5 * time.Minute
43+
pollingInterval = 5 * time.Second
44+
mockServer *httptest.Server
4445
)
4546

4647
BeforeAll(func() {
@@ -198,6 +199,8 @@ class AuthHandler(http.server.BaseHTTPRequestHandler):
198199
post_data = self.rfile.read(content_length)
199200
params = urllib.parse.parse_qs(post_data.decode('utf-8'))
200201
202+
# NOTE: Logging sensitive information (client_secret) is intentional for debugging E2E test failures.
203+
# This is test-only code and should NEVER be done in production environments.
201204
print(f"[{datetime.now()}] Token exchange request received:", flush=True)
202205
print(f" client_id: {params.get('client_id', [''])[0]}", flush=True)
203206
print(f" client_secret: {params.get('client_secret', [''])[0]}", flush=True)
@@ -580,6 +583,18 @@ with socketserver.TCPServer(("", PORT), OIDCHandler) as httpd:
580583
}
581584
Expect(k8sClient.Create(ctx, secret2)).To(Succeed())
582585

586+
// Secret for OIDC client secret
587+
oidcClientSecret := &corev1.Secret{
588+
ObjectMeta: metav1.ObjectMeta{
589+
Name: oidcClientSecretName,
590+
Namespace: testNamespace,
591+
},
592+
Data: map[string][]byte{
593+
"client-secret": []byte("vmcp-secret"),
594+
},
595+
}
596+
Expect(k8sClient.Create(ctx, oidcClientSecret)).To(Succeed())
597+
583598
By("Creating MCPExternalAuthConfig for token exchange")
584599
// Use the Kubernetes service URL for our mock auth server
585600
tokenURL := fmt.Sprintf("http://mock-auth-server.%s.svc.cluster.local/token", testNamespace)
@@ -662,14 +677,17 @@ with socketserver.TCPServer(("", PORT), OIDCHandler) as httpd:
662677
Transport: "streamable-http",
663678
ProxyPort: 8080,
664679
McpPort: 8080,
665-
// OIDC incoming auth - clients must authenticate with OIDC token
680+
// OIDC incoming auth - clients (including vMCP) must authenticate with OIDC token
666681
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
667682
Type: "inline",
668683
Inline: &mcpv1alpha1.InlineOIDCConfig{
669-
Issuer: fmt.Sprintf("http://mock-oidc-server.%s.svc.cluster.local", testNamespace),
670-
Audience: "test-audience",
671-
ClientID: "vmcp-client",
672-
ClientSecret: "vmcp-secret", // For testing only - use ClientSecretRef in production
684+
Issuer: fmt.Sprintf("http://mock-oidc-server.%s.svc.cluster.local", testNamespace),
685+
Audience: "test-audience",
686+
ClientID: "vmcp-client",
687+
ClientSecretRef: &mcpv1alpha1.SecretKeyRef{
688+
Name: oidcClientSecretName,
689+
Key: "client-secret",
690+
},
673691
InsecureAllowHTTP: true,
674692
JWKSAllowPrivateIP: true,
675693
ProtectedResourceAllowPrivateIP: true,
@@ -797,17 +815,35 @@ with socketserver.TCPServer(("", PORT), OIDCHandler) as httpd:
797815
By("Waiting for VirtualMCPServer to be ready")
798816
WaitForVirtualMCPServerReady(ctx, k8sClient, vmcpServerName, testNamespace, timeout)
799817

800-
// Give vMCP a moment to initialize and perform initial token exchange
801-
By("Waiting for vMCP to perform initial token exchange")
802-
time.Sleep(5 * time.Second)
818+
// Wait for vMCP pods to be fully running and ready
819+
By("Waiting for vMCP pods to be running and ready")
820+
vmcpLabels := map[string]string{
821+
"app.kubernetes.io/name": "virtualmcpserver",
822+
"app.kubernetes.io/instance": vmcpServerName,
823+
}
824+
WaitForPodsReady(ctx, k8sClient, testNamespace, vmcpLabels, timeout)
803825
})
804826

805827
AfterAll(func() {
806-
By("Shutting down mock HTTP server")
807-
// mockServer is now a Kubernetes service, no need to close
808-
// if mockServer != nil {
809-
// mockServer.Close()
810-
// }
828+
By("Cleaning up mock HTTP server")
829+
_ = k8sClient.Delete(ctx, &corev1.Pod{
830+
ObjectMeta: metav1.ObjectMeta{
831+
Name: "mock-http-server",
832+
Namespace: testNamespace,
833+
},
834+
})
835+
_ = k8sClient.Delete(ctx, &corev1.Service{
836+
ObjectMeta: metav1.ObjectMeta{
837+
Name: "mock-http-server",
838+
Namespace: testNamespace,
839+
},
840+
})
841+
_ = k8sClient.Delete(ctx, &corev1.ConfigMap{
842+
ObjectMeta: metav1.ObjectMeta{
843+
Name: "mock-http-server-code",
844+
Namespace: testNamespace,
845+
},
846+
})
811847

812848
By("Cleaning up mock auth server")
813849
_ = k8sClient.Delete(ctx, &corev1.Pod{
@@ -1295,23 +1331,31 @@ with socketserver.TCPServer(("", PORT), OIDCHandler) as httpd:
12951331

12961332
Expect(calledTokenExchangeTool).To(BeTrue(), "Should have called at least one tool from token exchange backend")
12971333

1298-
// Give the auth server a moment to receive requests
1299-
// Token exchange may happen during vMCP startup or initialization, not necessarily during tool calls
1300-
time.Sleep(5 * time.Second)
1301-
13021334
By("Checking mock auth server logs for token exchange requests")
13031335
authServerPodName := "mock-auth-server"
13041336

1305-
// Get logs from the mock auth server
1306-
logs, err := GetPodLogs(ctx, k8sClient, authServerPodName, testNamespace, "auth-server")
1307-
Expect(err).ToNot(HaveOccurred(), "Should be able to get logs from mock auth server")
1337+
// Wait for auth server to receive and log token exchange requests
1338+
// Token exchange may happen during vMCP startup or initialization, not necessarily during tool calls
1339+
var logs string
1340+
Eventually(func() bool {
1341+
var err error
1342+
logs, err = GetPodLogs(ctx, authServerPodName, testNamespace, "auth-server")
1343+
if err != nil {
1344+
GinkgoWriter.Printf("Unable to get auth server logs: %v\n", err)
1345+
return false
1346+
}
1347+
// Check if logs contain evidence of token exchange
1348+
return strings.Contains(logs, "Token exchange") || strings.Contains(logs, "token") || len(logs) > 100
1349+
}, 30*time.Second, 2*time.Second).Should(BeTrue(), "Auth server should have received requests")
1350+
1351+
Expect(logs).ToNot(BeEmpty(), "Should have logs from mock auth server")
13081352

13091353
GinkgoWriter.Printf("\n=== Mock Auth Server Logs ===\n%s\n", logs)
13101354

13111355
// Also check vMCP logs to see if it's attempting token exchange
13121356
By("Checking vMCP logs for token exchange activity")
13131357
vmcpPodName := fmt.Sprintf("vmcp-%s-0", vmcpServerName)
1314-
vmcpLogs, vmcpErr := GetPodLogs(ctx, k8sClient, vmcpPodName, testNamespace, "vmcp")
1358+
vmcpLogs, vmcpErr := GetPodLogs(ctx, vmcpPodName, testNamespace, "vmcp")
13151359
if vmcpErr == nil {
13161360
GinkgoWriter.Printf("\n=== vMCP Logs (last 2000 chars) ===\n")
13171361
if len(vmcpLogs) > 2000 {
@@ -1431,7 +1475,7 @@ with socketserver.TCPServer(("", PORT), OIDCHandler) as httpd:
14311475

14321476
By("Checking vMCP logs for authentication activity")
14331477
vmcpPodName := fmt.Sprintf("vmcp-%s-0", vmcpServerName)
1434-
vmcpLogs, err := GetPodLogs(ctx, k8sClient, vmcpPodName, testNamespace, "vmcp")
1478+
vmcpLogs, err := GetPodLogs(ctx, vmcpPodName, testNamespace, "vmcp")
14351479
Expect(err).ToNot(HaveOccurred(), "Should be able to get vMCP logs")
14361480

14371481
// Check for authentication-related log messages
@@ -1457,7 +1501,7 @@ with socketserver.TCPServer(("", PORT), OIDCHandler) as httpd:
14571501
// Also check auth server logs to verify token exchange happened
14581502
By("Verifying token exchange with auth server")
14591503
authServerPodName := mockAuthServerName
1460-
authLogs, err := GetPodLogs(ctx, k8sClient, authServerPodName, testNamespace, "auth-server")
1504+
authLogs, err := GetPodLogs(ctx, authServerPodName, testNamespace, "auth-server")
14611505
if err == nil {
14621506
hasTokenRequest := strings.Contains(authLogs, "Token exchange request received")
14631507
GinkgoWriter.Printf("Token exchange requests to auth server: %v\n", hasTokenRequest)

0 commit comments

Comments
 (0)