Skip to content

Commit e7d152b

Browse files
committed
Add explicit unauthenticated strategy for vMCP
Replace the pattern of passing nil authenticators with an explicit UnauthenticatedStrategy that implements the Strategy interface as a no-op. This makes the intent clear in configuration and improves type safety by eliminating nil checks. The strategy is appropriate for backends on trusted networks or where authentication is handled at the network layer. Configuration now explicitly declares "strategy: unauthenticated" instead of relying on implicit nil behavior.
1 parent cecac51 commit e7d152b

File tree

2 files changed

+268
-0
lines changed

2 files changed

+268
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package strategies
2+
3+
import (
4+
"context"
5+
"net/http"
6+
)
7+
8+
// UnauthenticatedStrategy is a no-op authentication strategy that performs no authentication.
9+
// This strategy is used when a backend MCP server requires no authentication.
10+
//
11+
// Unlike passing a nil authenticator (which is now an error), this strategy makes
12+
// the intent explicit: "this backend intentionally has no authentication".
13+
//
14+
// The strategy performs no modifications to requests and validates all metadata.
15+
//
16+
// This is appropriate when:
17+
// - The backend MCP server is on a trusted network (e.g., localhost)
18+
// - The backend has no authentication requirements
19+
// - Authentication is handled by network-level security (e.g., VPC, firewall)
20+
//
21+
// Security Warning: Only use this strategy when you are certain the backend
22+
// requires no authentication. For production deployments, prefer explicit
23+
// authentication strategies (pass_through, header_injection, token_exchange).
24+
//
25+
// Configuration: No metadata required, but any metadata is accepted and ignored.
26+
//
27+
// Example configuration:
28+
//
29+
// backends:
30+
// local-backend:
31+
// strategy: "unauthenticated"
32+
type UnauthenticatedStrategy struct{}
33+
34+
// NewUnauthenticatedStrategy creates a new UnauthenticatedStrategy instance.
35+
func NewUnauthenticatedStrategy() *UnauthenticatedStrategy {
36+
return &UnauthenticatedStrategy{}
37+
}
38+
39+
// Name returns the strategy identifier.
40+
func (*UnauthenticatedStrategy) Name() string {
41+
return "unauthenticated"
42+
}
43+
44+
// Authenticate performs no authentication and returns immediately.
45+
//
46+
// This method:
47+
// 1. Does not modify the request in any way
48+
// 2. Always returns nil (success)
49+
//
50+
// Parameters:
51+
// - ctx: Request context (unused)
52+
// - req: The HTTP request (not modified)
53+
// - metadata: Strategy-specific configuration (ignored)
54+
//
55+
// Returns nil (always succeeds).
56+
func (*UnauthenticatedStrategy) Authenticate(_ context.Context, _ *http.Request, _ map[string]any) error {
57+
// No-op: intentionally does nothing
58+
return nil
59+
}
60+
61+
// Validate checks if the strategy configuration is valid.
62+
//
63+
// UnauthenticatedStrategy accepts any metadata (including nil or empty),
64+
// so this always returns nil.
65+
//
66+
// This permissive validation allows the strategy to be used without
67+
// configuration or with arbitrary configuration that may be present
68+
// for documentation purposes.
69+
func (*UnauthenticatedStrategy) Validate(_ map[string]any) error {
70+
// No-op: accepts any metadata
71+
return nil
72+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package strategies
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestUnauthenticatedStrategy_Name(t *testing.T) {
14+
t.Parallel()
15+
16+
strategy := NewUnauthenticatedStrategy()
17+
assert.Equal(t, "unauthenticated", strategy.Name())
18+
}
19+
20+
func TestUnauthenticatedStrategy_Authenticate(t *testing.T) {
21+
t.Parallel()
22+
23+
tests := []struct {
24+
name string
25+
metadata map[string]any
26+
setupRequest func() *http.Request
27+
checkRequest func(t *testing.T, req *http.Request)
28+
}{
29+
{
30+
name: "does not modify request with no metadata",
31+
metadata: nil,
32+
setupRequest: func() *http.Request {
33+
req := httptest.NewRequest(http.MethodGet, "http://backend.example.com/test", nil)
34+
req.Header.Set("X-Custom-Header", "original-value")
35+
return req
36+
},
37+
checkRequest: func(t *testing.T, req *http.Request) {
38+
t.Helper()
39+
// Original headers should be unchanged
40+
assert.Equal(t, "original-value", req.Header.Get("X-Custom-Header"))
41+
// No auth headers should be added
42+
assert.Empty(t, req.Header.Get("Authorization"))
43+
},
44+
},
45+
{
46+
name: "does not modify request with metadata present",
47+
metadata: map[string]any{
48+
"some_key": "some_value",
49+
"count": 42,
50+
},
51+
setupRequest: func() *http.Request {
52+
req := httptest.NewRequest(http.MethodGet, "http://backend.example.com/test", nil)
53+
req.Header.Set("X-Existing", "existing-value")
54+
return req
55+
},
56+
checkRequest: func(t *testing.T, req *http.Request) {
57+
t.Helper()
58+
// Original headers should be unchanged
59+
assert.Equal(t, "existing-value", req.Header.Get("X-Existing"))
60+
// No auth headers should be added
61+
assert.Empty(t, req.Header.Get("Authorization"))
62+
},
63+
},
64+
{
65+
name: "preserves existing Authorization header",
66+
metadata: nil,
67+
setupRequest: func() *http.Request {
68+
req := httptest.NewRequest(http.MethodGet, "http://backend.example.com/test", nil)
69+
req.Header.Set("Authorization", "Bearer existing-token")
70+
return req
71+
},
72+
checkRequest: func(t *testing.T, req *http.Request) {
73+
t.Helper()
74+
// Should not modify existing Authorization header
75+
assert.Equal(t, "Bearer existing-token", req.Header.Get("Authorization"))
76+
},
77+
},
78+
{
79+
name: "works with empty request",
80+
metadata: nil,
81+
setupRequest: func() *http.Request {
82+
return httptest.NewRequest(http.MethodGet, "http://backend.example.com/test", nil)
83+
},
84+
checkRequest: func(t *testing.T, req *http.Request) {
85+
t.Helper()
86+
// Request should have no auth headers
87+
assert.Empty(t, req.Header.Get("Authorization"))
88+
// Headers should be empty or minimal
89+
assert.LessOrEqual(t, len(req.Header), 1) // May have Host header
90+
},
91+
},
92+
}
93+
94+
for _, tt := range tests {
95+
t.Run(tt.name, func(t *testing.T) {
96+
t.Parallel()
97+
98+
strategy := NewUnauthenticatedStrategy()
99+
req := tt.setupRequest()
100+
ctx := context.Background()
101+
102+
err := strategy.Authenticate(ctx, req, tt.metadata)
103+
104+
require.NoError(t, err)
105+
tt.checkRequest(t, req)
106+
})
107+
}
108+
}
109+
110+
func TestUnauthenticatedStrategy_Validate(t *testing.T) {
111+
t.Parallel()
112+
113+
tests := []struct {
114+
name string
115+
metadata map[string]any
116+
}{
117+
{
118+
name: "accepts nil metadata",
119+
metadata: nil,
120+
},
121+
{
122+
name: "accepts empty metadata",
123+
metadata: map[string]any{},
124+
},
125+
{
126+
name: "accepts arbitrary metadata",
127+
metadata: map[string]any{
128+
"key1": "value1",
129+
"key2": 42,
130+
"key3": []string{"a", "b", "c"},
131+
"nested": map[string]any{"inner": "value"},
132+
},
133+
},
134+
{
135+
name: "accepts metadata with typical auth fields",
136+
metadata: map[string]any{
137+
"token_url": "https://example.com/token",
138+
"client_id": "client-123",
139+
"header_name": "X-API-Key",
140+
},
141+
},
142+
}
143+
144+
for _, tt := range tests {
145+
t.Run(tt.name, func(t *testing.T) {
146+
t.Parallel()
147+
148+
strategy := NewUnauthenticatedStrategy()
149+
err := strategy.Validate(tt.metadata)
150+
151+
require.NoError(t, err)
152+
})
153+
}
154+
}
155+
156+
func TestUnauthenticatedStrategy_IntegrationBehavior(t *testing.T) {
157+
t.Parallel()
158+
159+
t.Run("strategy can be called multiple times safely", func(t *testing.T) {
160+
t.Parallel()
161+
162+
strategy := NewUnauthenticatedStrategy()
163+
ctx := context.Background()
164+
165+
// Call multiple times with different requests
166+
for i := 0; i < 5; i++ {
167+
req := httptest.NewRequest(http.MethodGet, "http://backend.example.com/test", nil)
168+
err := strategy.Authenticate(ctx, req, nil)
169+
require.NoError(t, err)
170+
assert.Empty(t, req.Header.Get("Authorization"))
171+
}
172+
})
173+
174+
t.Run("strategy is safe for concurrent use", func(t *testing.T) {
175+
t.Parallel()
176+
177+
strategy := NewUnauthenticatedStrategy()
178+
ctx := context.Background()
179+
180+
// Run authentication concurrently
181+
done := make(chan bool, 10)
182+
for i := 0; i < 10; i++ {
183+
go func() {
184+
req := httptest.NewRequest(http.MethodGet, "http://backend.example.com/test", nil)
185+
err := strategy.Authenticate(ctx, req, nil)
186+
assert.NoError(t, err)
187+
done <- true
188+
}()
189+
}
190+
191+
// Wait for all goroutines
192+
for i := 0; i < 10; i++ {
193+
<-done
194+
}
195+
})
196+
}

0 commit comments

Comments
 (0)