Skip to content

Commit 20712d5

Browse files
committed
Add execution proxy for code execution sandboxes
Implements OAuth proxy pattern allowing code execution sandboxes to call external APIs without exposing real user credentials. Short-lived execution tokens are issued, validated, and swapped for user credentials by mcp-front. Architecture: - User authenticates to external service (e.g., Datadog) via OAuth - Before code execution, request short-lived token from mcp-front - Sandbox uses token to call mcp-front proxy endpoint - mcp-front validates token and proxies with real user credentials Components: - internal/executiontoken: Token generation and validation using existing crypto.TokenSigner infrastructure (architectural win - no new dependencies) - internal/proxy: HTTP reverse proxy with token validation and path matching supporting glob patterns (*, **) - internal/server/execution_handlers: Token issuance endpoint requiring OAuth - Configuration: New "proxy" section per MCP server with baseURL, timeout, defaultAllowedPaths Endpoints: - POST /api/execution-token: Issue token (requires OAuth authentication) - ANY /proxy/{service}/*: Proxy requests (validates execution token) Security: - Short-lived tokens (5-15 min TTL, default 5 min) - Service scoping (token valid for one service only) - Path allowlisting with glob patterns - HMAC-signed tokens - Defense in depth with multiple validation layers - Complete audit trail Integration: - Reuses existing crypto.TokenSigner (browser state tokens use same mechanism) - Follows existing middleware patterns (CORS, logging, recovery) - Zero breaking changes (new endpoints are opt-in) - ~900 LOC production code, ~600 LOC tests Example config: { "mcpServers": { "datadog": { "userAuthentication": {"type": "oauth", ...}, "proxy": { "enabled": true, "baseURL": "https://api.datadoghq.com", "timeout": 30, "defaultAllowedPaths": ["/api/v1/**"] } } } } See EXECUTION_PROXY.md for complete documentation and integration examples.
1 parent 96ffe7d commit 20712d5

File tree

12 files changed

+2071
-0
lines changed

12 files changed

+2071
-0
lines changed

EXECUTION_PROXY.md

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
# Execution Proxy for Code Execution Sandboxes
2+
3+
## Overview
4+
5+
The execution proxy feature allows code execution sandboxes to call external APIs without exposing real user credentials to the sandbox. This is achieved through short-lived execution tokens that are validated and swapped for real user credentials by mcp-front.
6+
7+
## Use Case
8+
9+
When a user runs code in a sandbox (e.g., Stainless code execution) that needs to call an external API (e.g., Datadog), the sandbox should not have direct access to the user's credentials. Instead:
10+
11+
1. User authenticates to the external service via mcp-front OAuth
12+
2. Before code execution, request a short-lived execution token from mcp-front
13+
3. Inject the execution token into the sandbox environment
14+
4. Configure the SDK to use mcp-front's proxy URL with the execution token
15+
5. mcp-front validates the token and proxies requests with the real user credentials
16+
17+
## Architecture
18+
19+
```
20+
User → Claude → MCP Server → Code Execution Sandbox
21+
22+
(SDK with execution token)
23+
24+
mcp-front Proxy (/proxy/{service})
25+
26+
(validates token, swaps for user credentials)
27+
28+
External API (e.g., Datadog)
29+
```
30+
31+
## Configuration
32+
33+
### Enable Proxy for a Service
34+
35+
Add a `proxy` section to your MCP server configuration:
36+
37+
```json
38+
{
39+
"mcpServers": {
40+
"datadog": {
41+
"transportType": "inline",
42+
"requiresUserToken": true,
43+
"userAuthentication": {
44+
"type": "oauth",
45+
"displayName": "Datadog",
46+
"clientId": {"$env": "DATADOG_CLIENT_ID"},
47+
"clientSecret": {"$env": "DATADOG_CLIENT_SECRET"},
48+
"authorizationUrl": "https://app.datadoghq.com/oauth2/v1/authorize",
49+
"tokenUrl": "https://app.datadoghq.com/oauth2/v1/token",
50+
"scopes": ["metrics_read", "logs_read"]
51+
},
52+
"proxy": {
53+
"enabled": true,
54+
"baseURL": "https://api.datadoghq.com",
55+
"timeout": 30,
56+
"defaultAllowedPaths": [
57+
"/api/v1/**",
58+
"/api/v2/metrics/**",
59+
"/api/v2/logs/**"
60+
]
61+
}
62+
}
63+
}
64+
}
65+
```
66+
67+
### Configuration Fields
68+
69+
- **`enabled`** (required): Set to `true` to enable the proxy for this service
70+
- **`baseURL`** (required): The base URL of the external API
71+
- **`timeout`** (optional): Request timeout in seconds (default: 30)
72+
- **`defaultAllowedPaths`** (optional): Default paths allowed for execution tokens
73+
74+
### Path Patterns
75+
76+
Path patterns support glob-style wildcards:
77+
78+
- `/api/v1/metrics` - Exact match
79+
- `/api/v1/*` - Match any path one level deep (e.g., `/api/v1/metrics`, `/api/v1/logs`)
80+
- `/api/**` - Match any path recursively (e.g., `/api/v1/metrics`, `/api/v1/metrics/query`)
81+
- `/api/*/metrics` - Match with wildcard in middle (e.g., `/api/v1/metrics`, `/api/v2/metrics`)
82+
83+
## API Endpoints
84+
85+
### POST /api/execution-token
86+
87+
Issue a new execution token for code execution.
88+
89+
**Authentication:** OAuth bearer token (user must be authenticated)
90+
91+
**Request Body:**
92+
93+
```json
94+
{
95+
"execution_id": "exec-abc123",
96+
"target_service": "datadog",
97+
"ttl_seconds": 300,
98+
"allowed_paths": ["/api/v1/metrics", "/api/v2/logs"],
99+
"max_requests": 1000
100+
}
101+
```
102+
103+
**Fields:**
104+
105+
- **`execution_id`** (required): Unique identifier for this execution
106+
- **`target_service`** (required): Name of the service to proxy to
107+
- **`ttl_seconds`** (optional): Token lifetime in seconds (default: 300, max: 900)
108+
- **`allowed_paths`** (optional): Paths allowed for this token (defaults to service config)
109+
- **`max_requests`** (optional): Maximum number of requests (not enforced in MVP)
110+
111+
**Response:**
112+
113+
```json
114+
{
115+
"token": "eyJ...",
116+
"proxy_url": "https://mcp-front.example.com/proxy/datadog",
117+
"expires_at": "2025-11-25T12:35:00Z"
118+
}
119+
```
120+
121+
**Errors:**
122+
123+
- `401 Unauthorized` - Missing or invalid OAuth token
124+
- `403 Forbidden` - User has not connected to target service
125+
- `404 Not Found` - Target service not configured
126+
- `400 Bad Request` - Invalid request or proxy not enabled for service
127+
128+
### ANY /proxy/{service}/{path}
129+
130+
Proxy requests to the target service.
131+
132+
**Authentication:** Execution token (Bearer in Authorization header)
133+
134+
**URL Format:** `/proxy/{service}/{path}`
135+
136+
**Example:**
137+
138+
```
139+
GET /proxy/datadog/api/v1/metrics?query=avg:cpu
140+
Authorization: Bearer eyJ...
141+
```
142+
143+
The request is proxied to:
144+
145+
```
146+
GET https://api.datadoghq.com/api/v1/metrics?query=avg:cpu
147+
Authorization: Bearer <user's-real-token>
148+
```
149+
150+
**Errors:**
151+
152+
- `401 Unauthorized` - Missing or invalid execution token
153+
- `403 Forbidden` - Path not allowed by token
154+
- `404 Not Found` - Service not configured
155+
- `502 Bad Gateway` - Backend service error
156+
- `504 Gateway Timeout` - Backend timeout
157+
158+
## Integration Example
159+
160+
### Stainless Code Execution
161+
162+
1. **Request execution token before running code:**
163+
164+
```bash
165+
curl -X POST https://mcp-front.example.com/api/execution-token \
166+
-H "Authorization: Bearer ${OAUTH_TOKEN}" \
167+
-H "Content-Type: application/json" \
168+
-d '{
169+
"execution_id": "exec-123",
170+
"target_service": "datadog",
171+
"ttl_seconds": 300
172+
}'
173+
```
174+
175+
Response:
176+
177+
```json
178+
{
179+
"token": "eyJ...",
180+
"proxy_url": "https://mcp-front.example.com/proxy/datadog",
181+
"expires_at": "2025-11-25T12:35:00Z"
182+
}
183+
```
184+
185+
2. **Inject into sandbox environment:**
186+
187+
```typescript
188+
// Template for code execution
189+
const executionToken = process.env.EXECUTION_TOKEN; // eyJ...
190+
const proxyURL = process.env.PROXY_URL; // https://mcp-front.example.com/proxy/datadog
191+
192+
// Initialize generated Datadog SDK
193+
const datadog = new DatadogSDK({
194+
baseURL: proxyURL,
195+
auth: `Bearer ${executionToken}`,
196+
});
197+
198+
// User's code runs here
199+
const metrics = await datadog.metrics.query({
200+
query: "avg:cpu.usage{*}",
201+
from: Date.now() - 3600000,
202+
to: Date.now()
203+
});
204+
205+
console.log(metrics);
206+
```
207+
208+
3. **SDK makes proxied request:**
209+
210+
```
211+
GET https://mcp-front.example.com/proxy/datadog/api/v1/metrics?query=avg:cpu.usage{*}&from=...
212+
Authorization: Bearer eyJ...
213+
```
214+
215+
4. **mcp-front validates token and proxies:**
216+
217+
```
218+
GET https://api.datadoghq.com/api/v1/metrics?query=avg:cpu.usage{*}&from=...
219+
Authorization: Bearer dd_api_key_abc123
220+
```
221+
222+
## Security
223+
224+
### Token Properties
225+
226+
- **Short-lived**: Default 5 minutes, maximum 15 minutes
227+
- **Service-scoped**: Token valid for one service only
228+
- **Path-restricted**: Optional path allowlisting via glob patterns
229+
- **HMAC-signed**: Same signing mechanism as browser session tokens
230+
- **Non-replayable**: Tokens expire after TTL
231+
232+
### Threat Mitigation
233+
234+
| Threat | Mitigation |
235+
|--------|-----------|
236+
| Token exfiltration | Very short TTL (5-15 min) |
237+
| Privilege escalation | Service scoping, path allowlisting |
238+
| Token forgery | HMAC-SHA256 signing |
239+
| Confused deputy | Service name validation in token |
240+
| DoS via proxy | Timeout enforcement, rate limiting (future) |
241+
| Credential leakage | Tokens never contain real credentials |
242+
243+
### Audit Trail
244+
245+
All proxy requests are logged with:
246+
247+
- Execution ID
248+
- User email
249+
- Target service
250+
- Request method and path
251+
- Response status
252+
- Duration
253+
254+
## Testing
255+
256+
### Unit Tests
257+
258+
```bash
259+
# Test execution token generation/validation
260+
go test ./internal/executiontoken -v
261+
262+
# Test path matching
263+
go test ./internal/proxy -v -run TestPathMatcher
264+
265+
# Test HTTP proxy
266+
go test ./internal/proxy -v -run TestHTTPProxy
267+
```
268+
269+
### Integration Test
270+
271+
```bash
272+
# End-to-end proxy flow
273+
go test ./integration -v -run TestExecutionProxy
274+
```
275+
276+
### Manual Testing
277+
278+
1. Start mcp-front with proxy-enabled service configuration
279+
2. Authenticate user via OAuth
280+
3. Request execution token:
281+
282+
```bash
283+
curl -X POST http://localhost:8080/api/execution-token \
284+
-H "Authorization: Bearer ${OAUTH_TOKEN}" \
285+
-H "Content-Type: application/json" \
286+
-d '{
287+
"execution_id": "test-123",
288+
"target_service": "datadog",
289+
"ttl_seconds": 300
290+
}'
291+
```
292+
293+
4. Use execution token to proxy request:
294+
295+
```bash
296+
curl http://localhost:8080/proxy/datadog/api/v1/metrics \
297+
-H "Authorization: Bearer ${EXECUTION_TOKEN}"
298+
```
299+
300+
## Monitoring
301+
302+
### Logs
303+
304+
Execution proxy logs are emitted with the `execution_proxy` prefix:
305+
306+
```
307+
INFO execution_proxy: Execution token issued {user=user@example.com execution_id=exec-123 target_service=datadog ttl_seconds=300}
308+
INFO execution_proxy: Request proxied successfully {execution_id=exec-123 user=user@example.com service=datadog method=GET path=/api/v1/metrics duration_ms=45}
309+
```
310+
311+
### Metrics (Future)
312+
313+
- `execution_tokens_issued_total{service}` - Total tokens issued
314+
- `execution_proxy_requests_total{service,status}` - Total proxy requests
315+
- `execution_proxy_duration_seconds{service}` - Request duration histogram
316+
- `execution_token_validations_total{result}` - Token validation results
317+
318+
## Troubleshooting
319+
320+
### Token validation fails
321+
322+
**Symptom:** `401 Unauthorized: invalid execution token`
323+
324+
**Causes:**
325+
- Token expired (check TTL)
326+
- Wrong signing key (verify JWT_SECRET)
327+
- Token tampered with
328+
- Service name mismatch
329+
330+
**Solution:** Request a new token
331+
332+
### Path not allowed
333+
334+
**Symptom:** `403 Forbidden: path /api/v3/metrics not allowed for this execution`
335+
336+
**Causes:**
337+
- Path not in token's `allowed_paths`
338+
- Path not in service's `defaultAllowedPaths`
339+
340+
**Solution:** Request token with correct `allowed_paths` or update service configuration
341+
342+
### User credentials not found
343+
344+
**Symptom:** `401 Unauthorized: user credentials not found for service datadog`
345+
346+
**Causes:**
347+
- User has not connected to the service via OAuth
348+
- User token expired and refresh failed
349+
350+
**Solution:** User must authenticate to the service via mcp-front OAuth flow
351+
352+
### Backend timeout
353+
354+
**Symptom:** `504 Gateway Timeout: Backend service unavailable`
355+
356+
**Causes:**
357+
- Backend service is slow or down
358+
- Timeout too short for operation
359+
360+
**Solution:** Increase `timeout` in proxy configuration
361+
362+
## Future Enhancements
363+
364+
### Phase 2 (Planned)
365+
366+
- Request rate limiting per execution token
367+
- Request counting enforcement (`max_requests`)
368+
- Token revocation API
369+
- Execution context tracking in storage
370+
371+
### Phase 3 (Future)
372+
373+
- Response filtering/transformation
374+
- Request/response logging to storage
375+
- Webhook notifications for security events
376+
- Custom path rewriting rules
377+
- Multi-region proxy support
378+
379+
## See Also
380+
381+
- [OAuth Configuration](./docs/oauth.md)
382+
- [MCP Server Configuration](./docs/mcp-servers.md)
383+
- [Security Best Practices](./docs/security.md)

0 commit comments

Comments
 (0)