Skip to content

Commit 6e3706d

Browse files
add proxy-friendly headers and address locking (#291)
* add proxy-friendly headers and address locking * add Authorization Bearer header support
1 parent a26a9a8 commit 6e3706d

File tree

3 files changed

+103
-8
lines changed

3 files changed

+103
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ FIXES
55
* Fix sessions handling in stateless and load balanced environments
66

77
IMPROVEMENTS
8-
8+
* Add `Authorization: Bearer` header support for Terraform token in proxy environments
99
* Add `--heartbeat-interval` CLI flag and `MCP_HEARTBEAT_INTERVAL` env var for HTTP heartbeat in load-balanced environments
1010
* Set custom User-Agent header for TFE API requests to enable tracking MCP server usage separately from other go-tfe clients [268](https://github.com/hashicorp/terraform-mcp-server/pull/268)
1111
* Adding a new cli flags `--log-level` to set the desired log level for the server logs and `--log-format` for the logs formatting [286](https://github.com/hashicorp/terraform-mcp-server/pull/286)

pkg/client/middleware.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"context"
88
"fmt"
99
"net/http"
10-
"net/textproto"
1110
"os"
1211
"strings"
1312

@@ -101,7 +100,7 @@ func (h *securityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
101100
w.Header().Set("Access-Control-Max-Age", "3600")
102101
w.Header().Set("Access-Control-Allow-Origin", origin)
103102
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
104-
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id")
103+
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Authorization")
105104
}
106105

107106
// Handle OPTIONS requests for CORS preflight
@@ -125,6 +124,15 @@ func NewSecurityHandler(handler http.Handler, allowedOrigins []string, corsMode
125124
}
126125
}
127126

127+
// getTokenFromAuthHeader extracts token from Authorization Bearer header
128+
func getTokenFromAuthHeader(r *http.Request) string {
129+
authHeader := r.Header.Get("Authorization")
130+
if strings.HasPrefix(authHeader, "Bearer ") {
131+
return strings.TrimPrefix(authHeader, "Bearer ")
132+
}
133+
return ""
134+
}
135+
128136
// TerraformContextMiddleware adds Terraform-related header values to the request context
129137
// This middleware extracts Terraform configuration from HTTP headers, query parameters,
130138
// or environment variables and adds them to the request context for use by MCP tools
@@ -134,16 +142,22 @@ func TerraformContextMiddleware(logger *log.Logger) func(http.Handler) http.Hand
134142
requiredHeaders := []string{TerraformAddress, TerraformToken, TerraformSkipTLSVerify}
135143
ctx := r.Context()
136144
for _, header := range requiredHeaders {
137-
// Priority order: HTTP header -> Query parameter -> Environment variable
138-
headerValue := r.Header.Get(textproto.CanonicalMIMEHeaderKey(header))
145+
var headerValue string
146+
147+
// Check standard header first
148+
headerValue = r.Header.Get(header)
149+
150+
// For token, also support Authorization: Bearer header as fallback
151+
if headerValue == "" && header == TerraformToken {
152+
headerValue = getTokenFromAuthHeader(r)
153+
}
139154

140155
if headerValue == "" {
141156
headerValue = r.URL.Query().Get(header)
142157

143-
// Explicitly disallow TerraformToken in query parameters for security reasons
144158
if header == TerraformToken && headerValue != "" {
145159
logger.Info(fmt.Sprintf("Terraform token was provided in query parameters by client %v, terminating request", r.RemoteAddr))
146-
http.Error(w, "Terraform token should not be provided in query parameters for security reasons, use the terraform_token header", http.StatusBadRequest)
160+
http.Error(w, "Terraform token should not be provided in query parameters for security reasons, use the Authorization header", http.StatusBadRequest)
147161
return
148162
}
149163
}

pkg/client/middleware_test.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,62 @@ func TestOptionsRequest(t *testing.T) {
315315
assert.NotEmpty(t, rr.Header().Get("Access-Control-Allow-Methods"))
316316
}
317317

318+
// TestGetTokenFromAuthHeader tests the helper function that extracts token from Authorization Bearer header
319+
func TestGetTokenFromAuthHeader(t *testing.T) {
320+
tests := []struct {
321+
name string
322+
headers map[string]string
323+
expected string
324+
}{
325+
{
326+
name: "Authorization Bearer token",
327+
headers: map[string]string{"Authorization": "Bearer my-token"},
328+
expected: "my-token",
329+
},
330+
{
331+
name: "Authorization Basic ignored",
332+
headers: map[string]string{"Authorization": "Basic abc123"},
333+
expected: "",
334+
},
335+
{
336+
name: "no Authorization header",
337+
headers: map[string]string{},
338+
expected: "",
339+
},
340+
{
341+
name: "empty Authorization header",
342+
headers: map[string]string{"Authorization": ""},
343+
expected: "",
344+
},
345+
{
346+
name: "Bearer with no token",
347+
headers: map[string]string{"Authorization": "Bearer "},
348+
expected: "",
349+
},
350+
{
351+
name: "Bearer with whitespace token",
352+
headers: map[string]string{"Authorization": "Bearer "},
353+
expected: " ",
354+
},
355+
{
356+
name: "Bearer lowercase",
357+
headers: map[string]string{"Authorization": "bearer my-token"},
358+
expected: "", // Case sensitive - must be "Bearer"
359+
},
360+
}
361+
362+
for _, tt := range tests {
363+
t.Run(tt.name, func(t *testing.T) {
364+
req := httptest.NewRequest(http.MethodGet, "/", nil)
365+
for k, v := range tt.headers {
366+
req.Header.Set(k, v)
367+
}
368+
result := getTokenFromAuthHeader(req)
369+
assert.Equal(t, tt.expected, result)
370+
})
371+
}
372+
}
373+
318374
// TestTerraformContextMiddleware tests the middleware that extracts Terraform configuration
319375
// from HTTP headers, query parameters, and environment variables and adds them to the request context
320376
func TestTerraformContextMiddleware(t *testing.T) {
@@ -369,6 +425,31 @@ func TestTerraformContextMiddleware(t *testing.T) {
369425
TerraformSkipTLSVerify: "true",
370426
},
371427
},
428+
{
429+
name: "Authorization Bearer header provides token",
430+
headers: map[string]string{
431+
"Authorization": "Bearer bearer-token",
432+
},
433+
queryParams: map[string]string{},
434+
envVars: map[string]string{},
435+
expectedStatus: http.StatusOK,
436+
expectedContextVals: map[string]string{
437+
TerraformToken: "bearer-token",
438+
},
439+
},
440+
{
441+
name: "standard header takes priority over Authorization Bearer",
442+
headers: map[string]string{
443+
TerraformToken: "standard-token",
444+
"Authorization": "Bearer bearer-token",
445+
},
446+
queryParams: map[string]string{},
447+
envVars: map[string]string{},
448+
expectedStatus: http.StatusOK,
449+
expectedContextVals: map[string]string{
450+
TerraformToken: "standard-token",
451+
},
452+
},
372453
{
373454
name: "query params take priority over env vars (except token)",
374455
headers: map[string]string{},
@@ -428,7 +509,7 @@ func TestTerraformContextMiddleware(t *testing.T) {
428509
envVars: map[string]string{},
429510
expectedStatus: http.StatusBadRequest,
430511
expectError: true,
431-
errorMessage: "Terraform token should not be provided in query parameters for security reasons, use the terraform_token header",
512+
errorMessage: "Terraform token should not be provided in query parameters",
432513
},
433514
{
434515
name: "canonical header names are handled correctly",

0 commit comments

Comments
 (0)