Skip to content

Commit 7e7e3b1

Browse files
committed
Implement HeaderInjection authentication strategy
Add HeaderInjectionStrategy for injecting static header values into backend requests. This general-purpose strategy supports any HTTP header with any static value, enabling flexible authentication schemes like API keys, bearer tokens, and custom auth headers. The strategy extracts header_name and api_key from metadata configuration and validates them to prevent CRLF injection attacks using pkg/validation functions. Validation occurs at configuration time for fail-fast behavior. Changes: - Add HeaderInjectionStrategy implementation with Authenticate/Validate - Include comprehensive test coverage (408 test lines) - Use ValidateHTTPHeaderName/Value for security checks - Prepared for future secret reference resolution
1 parent e7d152b commit 7e7e3b1

File tree

2 files changed

+521
-0
lines changed

2 files changed

+521
-0
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Package strategies provides authentication strategy implementations for Virtual MCP Server.
2+
package strategies
3+
4+
import (
5+
"context"
6+
"fmt"
7+
"net/http"
8+
9+
"github.com/stacklok/toolhive/pkg/validation"
10+
)
11+
12+
// HeaderInjectionStrategy injects a static header value into request headers.
13+
// This is a general-purpose strategy that can inject any header with any value,
14+
// commonly used for API keys, bearer tokens, or custom authentication headers.
15+
//
16+
// The strategy extracts the header name and value from the metadata
17+
// configuration and injects them into the backend request headers.
18+
//
19+
// Required metadata fields:
20+
// - header_name: The HTTP header name to use (e.g., "X-API-Key", "Authorization")
21+
// - api_key: The header value to inject (can be an API key, token, or any value)
22+
//
23+
// This strategy is appropriate when:
24+
// - The backend requires a static header value for authentication
25+
// - The header value is stored securely in the vMCP configuration
26+
// - No dynamic token exchange or user-specific authentication is required
27+
//
28+
// Future enhancements may include:
29+
// - Secret reference resolution (e.g., ${SECRET_REF:...})
30+
// - Support for multiple header formats (e.g., "Bearer <key>")
31+
// - Value rotation and refresh mechanisms
32+
type HeaderInjectionStrategy struct{}
33+
34+
// NewHeaderInjectionStrategy creates a new HeaderInjectionStrategy instance.
35+
func NewHeaderInjectionStrategy() *HeaderInjectionStrategy {
36+
return &HeaderInjectionStrategy{}
37+
}
38+
39+
// Name returns the strategy identifier.
40+
func (*HeaderInjectionStrategy) Name() string {
41+
return "header_injection"
42+
}
43+
44+
// Authenticate injects the header value from metadata into the request header.
45+
//
46+
// This method:
47+
// 1. Validates that header_name and api_key are present in metadata
48+
// 2. Sets the specified header with the provided value
49+
//
50+
// Parameters:
51+
// - ctx: Request context (currently unused, reserved for future secret resolution)
52+
// - req: The HTTP request to authenticate
53+
// - metadata: Strategy-specific configuration containing header_name and api_key
54+
//
55+
// Returns an error if:
56+
// - header_name is missing or empty
57+
// - api_key is missing or empty
58+
func (*HeaderInjectionStrategy) Authenticate(_ context.Context, req *http.Request, metadata map[string]any) error {
59+
headerName, ok := metadata["header_name"].(string)
60+
if !ok || headerName == "" {
61+
return fmt.Errorf("header_name required in metadata")
62+
}
63+
64+
apiKey, ok := metadata["api_key"].(string)
65+
if !ok || apiKey == "" {
66+
return fmt.Errorf("api_key required in metadata")
67+
}
68+
69+
// TODO: Future enhancement - resolve secret references
70+
// if strings.HasPrefix(apiKey, "${SECRET_REF:") {
71+
// apiKey, err = s.secretResolver.Resolve(ctx, apiKey)
72+
// if err != nil {
73+
// return fmt.Errorf("failed to resolve secret reference: %w", err)
74+
// }
75+
// }
76+
77+
req.Header.Set(headerName, apiKey)
78+
return nil
79+
}
80+
81+
// Validate checks if the required metadata fields are present and valid.
82+
//
83+
// This method verifies that:
84+
// - header_name is present and non-empty
85+
// - api_key is present and non-empty
86+
// - header_name is a valid HTTP header name (prevents CRLF injection)
87+
// - api_key is a valid HTTP header value (prevents CRLF injection)
88+
//
89+
// This validation is typically called during configuration parsing to fail fast
90+
// if the strategy is misconfigured.
91+
func (*HeaderInjectionStrategy) Validate(metadata map[string]any) error {
92+
headerName, ok := metadata["header_name"].(string)
93+
if !ok || headerName == "" {
94+
return fmt.Errorf("header_name required in metadata")
95+
}
96+
97+
apiKey, ok := metadata["api_key"].(string)
98+
if !ok || apiKey == "" {
99+
return fmt.Errorf("api_key required in metadata")
100+
}
101+
102+
// Validate header name to prevent injection attacks
103+
if err := validation.ValidateHTTPHeaderName(headerName); err != nil {
104+
return fmt.Errorf("invalid header_name: %w", err)
105+
}
106+
107+
// Validate API key value to prevent injection attacks
108+
if err := validation.ValidateHTTPHeaderValue(apiKey); err != nil {
109+
return fmt.Errorf("invalid api_key: %w", err)
110+
}
111+
112+
return nil
113+
}

0 commit comments

Comments
 (0)