Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Quick reference for AI agents working with MCP Gateway (Go-based MCP proxy serve
- `internal/launcher/` - Backend process management
- `internal/difc/` - Security labels (not enabled)
- `internal/guard/` - Security guards (NoopGuard active)
- `internal/auth/` - Authentication header parsing and middleware
- `internal/logger/` - Debug logging framework (micro logger)
- `internal/timeutil/` - Time formatting utilities

Expand Down Expand Up @@ -121,6 +122,7 @@ golangci-lint run --enable=gosec,testifylint --timeout=5m
**Add MCP Server**: Update config.toml with new server entry
**Add Route**: Edit `internal/server/routed.go` or `unified.go`
**Add Guard**: Implement in `internal/guard/` and register
**Add Auth Logic**: Implement in `internal/auth/` package
**Add Unit Test**: Create `*_test.go` in the appropriate `internal/` package
**Add Integration Test**: Create test in `test/integration/` that uses the binary

Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,34 @@ Supported JSON-RPC 2.0 methods:

## Security Features

### Authentication

MCPG implements MCP specification 7.1 for API key authentication.

**Authentication Header Format:**
- Per MCP spec 7.1: Authorization header MUST contain the API key directly
- Format: `Authorization: <api-key>` (plain API key, NOT Bearer scheme)
- Example: `Authorization: my-secret-api-key-123`

**Configuration:**
- Set via `MCP_GATEWAY_API_KEY` environment variable
- When configured, all endpoints except `/health` require authentication
- When not configured, authentication is disabled

**Implementation:**
- The `internal/auth` package provides centralized authentication logic
- `auth.ParseAuthHeader()` - Parses Authorization headers per MCP spec 7.1
- `auth.ValidateAPIKey()` - Validates provided API keys
- Backward compatibility for Bearer tokens is maintained

**Example Request:**
```bash
curl -X POST http://localhost:8000/mcp/github \
-H "Authorization: my-api-key" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
```

### Enhanced Error Debugging

Command failures now include extensive debugging information:
Expand Down
70 changes: 70 additions & 0 deletions internal/auth/header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Package auth provides authentication header parsing and middleware
// for the MCP Gateway server.
//
// This package implements MCP specification 7.1 for authentication,
// which requires Authorization headers to contain the API key directly
// without any scheme prefix (e.g., NOT "Bearer <key>").
//
// Example usage:
//
// apiKey, agentID, err := auth.ParseAuthHeader(r.Header.Get("Authorization"))
// if err != nil {
// // Handle error
// }
package auth

import (
"errors"
"strings"
)

var (
// ErrMissingAuthHeader is returned when the Authorization header is missing
ErrMissingAuthHeader = errors.New("missing Authorization header")
// ErrInvalidAuthHeader is returned when the Authorization header format is invalid
ErrInvalidAuthHeader = errors.New("invalid Authorization header format")
)

// ParseAuthHeader parses the Authorization header and extracts the API key and agent ID.
// Per MCP spec 7.1, the Authorization header should contain the API key directly
// without any Bearer prefix or other scheme.
//
// For backward compatibility, this function also supports:
// - "Bearer <token>" format (uses token as both API key and agent ID)
// - "Agent <agent-id>" format (extracts agent ID)
//
// Returns:
// - apiKey: The extracted API key
// - agentID: The extracted agent/session identifier
// - error: ErrMissingAuthHeader if header is empty, nil otherwise
func ParseAuthHeader(authHeader string) (apiKey string, agentID string, error error) {
if authHeader == "" {
return "", "", ErrMissingAuthHeader
}

// Handle "Bearer <token>" format (backward compatibility)
if strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimPrefix(authHeader, "Bearer ")
return token, token, nil
}

// Handle "Agent <agent-id>" format
if strings.HasPrefix(authHeader, "Agent ") {
agentIDValue := strings.TrimPrefix(authHeader, "Agent ")
return agentIDValue, agentIDValue, nil
}

// Per MCP spec 7.1: Authorization header contains API key directly
// Use the entire header value as both API key and agent/session ID
return authHeader, authHeader, nil
}

// ValidateAPIKey checks if the provided API key matches the expected key.
// Returns true if they match, false otherwise.
func ValidateAPIKey(provided, expected string) bool {
if expected == "" {
// No API key configured, authentication is disabled
return true
}
return provided == expected
}
105 changes: 105 additions & 0 deletions internal/auth/header_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package auth

import (
"testing"
)

func TestParseAuthHeader(t *testing.T) {
tests := []struct {
name string
authHeader string
wantAPIKey string
wantAgentID string
wantErr error
}{
{
name: "Empty header",
authHeader: "",
wantAPIKey: "",
wantAgentID: "",
wantErr: ErrMissingAuthHeader,
},
{
name: "Plain API key (MCP spec 7.1)",
authHeader: "my-secret-api-key",
wantAPIKey: "my-secret-api-key",
wantAgentID: "my-secret-api-key",
wantErr: nil,
},
{
name: "Bearer token (backward compatibility)",
authHeader: "Bearer my-token-123",
wantAPIKey: "my-token-123",
wantAgentID: "my-token-123",
wantErr: nil,
},
{
name: "Agent format",
authHeader: "Agent agent-123",
wantAPIKey: "agent-123",
wantAgentID: "agent-123",
wantErr: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotAPIKey, gotAgentID, gotErr := ParseAuthHeader(tt.authHeader)

if gotErr != tt.wantErr {
t.Errorf("ParseAuthHeader() error = %v, wantErr %v", gotErr, tt.wantErr)
return
}

if gotAPIKey != tt.wantAPIKey {
t.Errorf("ParseAuthHeader() gotAPIKey = %v, want %v", gotAPIKey, tt.wantAPIKey)
}

if gotAgentID != tt.wantAgentID {
t.Errorf("ParseAuthHeader() gotAgentID = %v, want %v", gotAgentID, tt.wantAgentID)
}
})
}
}

func TestValidateAPIKey(t *testing.T) {
tests := []struct {
name string
provided string
expected string
want bool
}{
{
name: "Matching keys",
provided: "my-secret-key",
expected: "my-secret-key",
want: true,
},
{
name: "Non-matching keys",
provided: "wrong-key",
expected: "correct-key",
want: false,
},
{
name: "Empty expected (auth disabled)",
provided: "any-key",
expected: "",
want: true,
},
{
name: "Empty provided with expected",
provided: "",
expected: "required-key",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ValidateAPIKey(tt.provided, tt.expected); got != tt.want {
t.Errorf("ValidateAPIKey() = %v, want %v", got, tt.want)
}
})
}
}
6 changes: 5 additions & 1 deletion internal/guard/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ func SetAgentIDInContext(ctx context.Context, agentID string) context.Context {
}

// ExtractAgentIDFromAuthHeader extracts agent ID from Authorization header
// Supports formats:
//
// Note: For MCP spec 7.1 compliant parsing, see internal/auth.ParseAuthHeader()
// which provides centralized authentication header parsing.
//
// This function supports formats:
// - "Bearer <token>" - uses token as agent ID
// - "Agent <agent-id>" - uses agent-id directly
// - Any other format - uses the entire value as agent ID
Expand Down
4 changes: 4 additions & 0 deletions internal/server/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import (

// authMiddleware implements API key authentication per spec section 7.1
// Per spec: Authorization header MUST contain the API key directly (NOT Bearer scheme)
//
// For header parsing logic, see internal/auth package which provides:
// - ParseAuthHeader() for extracting API keys and agent IDs
// - ValidateAPIKey() for key validation
func authMiddleware(apiKey string, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Extract Authorization header
Expand Down