Skip to content

Conversation

@jhrozek
Copy link
Contributor

@jhrozek jhrozek commented Nov 4, 2025

This PR implements the outgoing authentication infrastructure for Virtual MCP Server, enabling vMCP to authenticate requests to downstream backend MCP servers using configurable authentication strategies.

Overview

This work builds upon the incoming authentication foundation (merged in #2393) by adding the outgoing authentication boundary where vMCP authenticates TO backend servers. The implementation follows a strategy pattern with a registry-based architecture that separates configuration-time setup from request-time authentication for optimal performance.

Commit Breakdown

  1. Reorganize incoming auth factory into subfolder - Moves incoming authentication factory code to pkg/vmcp/auth/factory/ subfolder to prepare for adding outgoing authentication factory and prevent package organization issues.

  2. Add factory package to resolve auth import cycle - Creates pkg/vmcp/auth/factory package to break circular dependencies between auth interfaces and strategy implementations, allowing the factory to compose both without import cycles.

  3. Refactor outgoing auth to separate registry from strategy - Renames OutgoingAuthenticator to OutgoingAuthRegistry and removes the AuthenticateRequest() method, clarifying that the registry manages strategies while Strategy implementations perform actual authentication, improving performance by eliminating indirection.

  4. Integrate authentication registry into HTTP backend client - Wires authentication into HTTPBackendClient via authRoundTripper middleware that resolves strategies at client creation time (cold path) and applies them per-request (hot path), enabling fail-fast validation and 67% reduction in per-request overhead.

  5. Apply auth configuration in backend discoverer - Extends the CLI backend discoverer to accept authentication configuration and populate Backend.AuthStrategy and Backend.AuthMetadata fields with proper precedence (backend-specific > default > unauthenticated).

  6. Complete outgoing authentication integration in serve command - Connects all components in the serve command by creating the auth registry from configuration, passing it to the discoverer and HTTP client, completing the end-to-end authentication flow.

  7. Add explicit unauthenticated strategy for vMCP - Introduces UnauthenticatedStrategy as an explicit no-op implementation to replace nil authenticator patterns, improving type safety and making intent clear in configuration.

  8. Implement HeaderInjection authentication strategy - Adds the first concrete strategy implementation that injects static HTTP headers for API key and bearer token authentication, with CRLF injection protection and comprehensive test coverage.

Architecture

The implementation uses a Registry + Strategy pattern:

  • Registry (OutgoingAuthRegistry): Manages strategy registration and lookup
  • Strategy (Strategy interface): Performs authentication by modifying HTTP requests
  • Factory (auth/factory): Composes registry with strategies from configuration
  • RoundTripper (authRoundTripper): Applies strategies to outgoing requests

Performance characteristics:

  • Configuration validation at startup (fail-fast)
  • Strategy resolution at client creation (cold path)
  • Authentication execution per-request (hot path, minimal overhead)

Testing

  • Comprehensive unit tests for all components (registry, strategies, client integration)
  • Mock-based tests using gomock for declarative test structure
  • Security validation tests for CRLF injection prevention
  • Thread-safety tests for concurrent registry access

Next Steps

Future PRs will add additional authentication strategies:

  • PassThrough (forward client credentials)
  • TokenExchange (OAuth token exchange flow)
  • Additional strategies as needed

Related to Epic #150 (Backend Authentication).

@jhrozek jhrozek force-pushed the vmcp_out_strategies branch from 7e7e3b1 to 3131412 Compare November 4, 2025 14:08
@jhrozek jhrozek requested a review from Copilot November 4, 2025 14:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the outgoing authentication system from an OutgoingAuthenticator interface to an OutgoingAuthRegistry interface. The key changes involve:

  • Renaming and simplifying the authentication interface to focus on strategy registration/retrieval
  • Moving authentication logic directly to Strategy implementations called per-request
  • Introducing concrete strategy implementations (UnauthenticatedStrategy, HeaderInjectionStrategy)
  • Adding factory functions for creating authentication registries from configuration
  • Updating documentation and comments to reflect the new interface naming

Reviewed Changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/vmcp/types.go Updated comment to reference OutgoingAuthRegistry interface
pkg/vmcp/doc.go Updated interface documentation and usage examples with new naming and simplified API
pkg/vmcp/auth/auth.go Renamed OutgoingAuthenticator to OutgoingAuthRegistry, removed AuthenticateRequest method
pkg/vmcp/auth/outgoing_registry.go Renamed DefaultOutgoingAuthenticator to DefaultOutgoingAuthRegistry
pkg/vmcp/auth/outgoing_registry_test.go Renamed test suite for registry (was outgoing_authenticator_test.go)
pkg/vmcp/auth/strategies/unauthenticated.go Added UnauthenticatedStrategy implementation for no-auth backends
pkg/vmcp/auth/strategies/unauthenticated_test.go Added comprehensive tests for UnauthenticatedStrategy
pkg/vmcp/auth/strategies/header_injection.go Added HeaderInjectionStrategy for static header injection with CRLF validation
pkg/vmcp/auth/strategies/header_injection_test.go Added comprehensive tests for HeaderInjectionStrategy
pkg/vmcp/auth/factory/outgoing.go Added factory function for creating OutgoingAuthRegistry from configuration
pkg/vmcp/auth/factory/incoming.go Moved incoming auth middleware factory to factory package
pkg/vmcp/auth/factory/incoming_test.go Added tests for incoming auth middleware factory
pkg/vmcp/client/client.go Updated client to use OutgoingAuthRegistry and added authRoundTripper for per-request auth
pkg/vmcp/client/client_test.go Added comprehensive tests for auth integration in client
pkg/vmcp/client/mocks/mock_outgoing_registry.go Generated mock for OutgoingAuthRegistry interface
pkg/vmcp/aggregator/cli_discoverer.go Updated to accept auth config and populate backend auth fields
pkg/vmcp/aggregator/cli_discoverer_test.go Updated tests to pass nil auth config
cmd/vmcp/app/commands.go Updated to use factory package and create OutgoingAuthRegistry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov
Copy link

codecov bot commented Nov 4, 2025

Codecov Report

❌ Patch coverage is 68.55670% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.38%. Comparing base (5326b9b) to head (f1dd6a2).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
pkg/vmcp/client/client.go 63.82% 14 Missing and 3 partials ⚠️
pkg/vmcp/aggregator/cli_discoverer.go 11.11% 15 Missing and 1 partial ⚠️
cmd/vmcp/app/commands.go 0.00% 9 Missing ⚠️
pkg/vmcp/auth/factory/outgoing.go 88.46% 3 Missing and 3 partials ⚠️
pkg/vmcp/config/yaml_loader.go 14.28% 6 Missing ⚠️
pkg/vmcp/auth/outgoing_registry.go 87.50% 2 Missing and 1 partial ⚠️
pkg/vmcp/auth/strategies/header_injection.go 92.00% 1 Missing and 1 partial ⚠️
pkg/vmcp/config/validator.go 33.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2451      +/-   ##
==========================================
+ Coverage   55.33%   55.38%   +0.05%     
==========================================
  Files         278      281       +3     
  Lines       26470    26607     +137     
==========================================
+ Hits        14646    14736      +90     
- Misses      10479    10516      +37     
- Partials     1345     1355      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jhrozek
Copy link
Contributor Author

jhrozek commented Nov 4, 2025

@claude review this PR. Don't be overly verbose. Focus on security aspects as well as how well the code integrates into the vMCP gateway flow.

@claude
Copy link
Contributor

claude bot commented Nov 4, 2025

Claude finished @jhrozek's task —— View job


PR Review: Outgoing Authentication Strategies

Reviewing this PR with focus on security and vMCP gateway integration.

Review Checklist

  • Read core authentication interfaces and registry implementation
  • Review authentication strategy implementations (security focus)
  • Analyze integration into vMCP gateway flow
  • Review test coverage and quality
  • Provide feedback summary

Security Review

Strengths

  1. CRLF Injection Protection: HeaderInjectionStrategy properly validates both header names and values using pkg/validation (validation.go:50-88), which leverages Go's httpguts package. This prevents HTTP header injection attacks.

  2. Fail-Fast Validation: The architecture validates auth configuration at startup (cold path) rather than per-request (hot path), catching misconfigurations early (client.go:131-146).

  3. No Nil Authenticators: The explicit UnauthenticatedStrategy eliminates nil pointer risks and makes intent clear (strategies/unauthenticated.go:8-32).

  4. DoS Protection: Existing 100MB response size limit in client remains intact (client.go:24-40).

  5. Thread-Safety: Registry uses sync.RWMutex correctly for concurrent access (outgoing_registry.go:9-30).

  6. Request Cloning: authRoundTripper properly clones requests before modification to prevent race conditions (client.go:96-97).

Security Concerns

MEDIUM - Credential Exposure in Logs

The debug logging in client.go:104 logs the strategy name, which is safe, but earlier at cli_discoverer.go:111 there's logging that could inadvertently expose metadata structure. While this doesn't log the actual api_key value, be cautious about any future changes that might log metadata.

Recommendation: Add a comment warning against logging authMetadata contents.

LOW - No Secret Reference Support Yet

The TODO comment at header_injection.go:69-75 indicates planned secret reference resolution (${SECRET_REF:...}). Current implementation stores credentials in plain text in configuration.

Recommendation: Prioritize secret reference implementation or document that credentials should use external secret management.

LOW - Strategy Name Validation

The registry enforces that strategy.Name() matches the registration name (outgoing_registry.go:66-68), but this validation is not documented on the Strategy interface itself. The Copilot review comment correctly identified this.

Recommendation: Add this requirement to the Strategy interface godoc in auth.go:56-68.


Integration Analysis

Gateway Flow

The authentication integration follows a clean cold-path/hot-path split:

  1. Startup (Cold Path):

    • commands.go:218 → Factory creates registry from config
    • commands.go:224 → Discoverer applies auth config to backends
    • commands.go:240 → Client receives registry
    • client.go:136 → Strategy resolved and validated once per backend
  2. Request Time (Hot Path):

    • client.go:95-106 → authRoundTripper applies pre-resolved strategy
    • Minimal overhead: one Authenticate() call per request

This design is excellent for performance - configuration errors fail at startup, not during production requests.

Configuration Precedence

The discoverer correctly implements precedence (cli_discoverer.go:139-160):

  1. Backend-specific auth (highest priority)
  2. Default auth
  3. Unauthenticated (implicit fallback)

The factory always registers "unauthenticated" (factory/outgoing.go:52-55), ensuring safe behavior even with nil config.

Integration Points

  • Well-contained: Auth integration touches only necessary boundaries (factory, discoverer, client)
  • No breaking changes: Existing code paths unaffected
  • Testable: Mock-based testing enables isolated component validation

Test Coverage Analysis

Coverage: 45.90% patch coverage (99 lines missing)

High-Quality Tests

  • header_injection_test.go: Comprehensive strategy testing including CRLF validation, special characters, edge cases
  • unauthenticated_test.go: Complete coverage of no-op strategy
  • client_test.go: Good integration tests with mocks

Coverage Gaps

The main coverage gap is factory/outgoing.go (0% covered per codecov):

  • Factory logic for creating registry from config
  • Strategy type resolution (createStrategy)
  • Error paths for unknown strategy types

This is acceptable because:

  1. Factory code is straightforward with minimal business logic
  2. Integration is tested end-to-end in commands.go (though not unit tested)
  3. Invalid strategy types fail fast at startup

Recommendation: Consider adding factory unit tests in a follow-up PR, especially for error cases (unknown strategy types, validation failures).


Code Quality Observations

Excellent

  • Clean separation of concerns (registry vs strategy vs factory)
  • Comprehensive documentation and godoc comments
  • Consistent error messages with context
  • Good use of Go idioms (RWMutex, functional options pattern)

Minor Issues

  1. Unused Context: NewOutgoingAuthRegistry accepts context.Context but doesn't use it (factory/outgoing.go:49). The underscore prefix correctly indicates this, but consider removing if truly unnecessary.

  2. Error Wrapping Consistency: Most errors properly wrap with %w, but some could be more specific (e.g., cli_discoverer.go:158 returns empty string/nil rather than explicit "unauthenticated" strategy name).


Verdict

APPROVE with minor recommendations

This is a well-architected implementation with strong security practices. The registry + strategy pattern provides clean extensibility for future auth types (PassThrough, TokenExchange). The cold-path validation ensures production reliability.

Key strengths:

  • Solid security foundations (CRLF protection, validation, fail-fast)
  • Excellent performance characteristics
  • Clean integration into vMCP gateway flow
  • Good test coverage where it matters most

Recommendations for follow-up:

  1. Add factory unit tests for error paths
  2. Document strategy name matching requirement in interface
  3. Add comment warning about credential logging
  4. Prioritize secret reference resolution

The 45% patch coverage is primarily due to untested factory code, which is acceptable given the straightforward nature of that code and the end-to-end testing elsewhere.


Branch: vmcp_out_strategies

Copy link

Copilot AI commented Nov 4, 2025

@jhrozek I've opened a new pull request, #2454, to work on those changes. Once the pull request is ready, I'll request review from you.

…cycle

Move incoming authentication factory from pkg/vmcp/auth/ to
pkg/vmcp/auth/factory/ subfolder to improve code organization.
This separates factory code from core authentication types and
middleware.
Rename OutgoingAuthenticator to OutgoingAuthRegistry to better reflect
its responsibility as a strategy registry rather than an authenticator.

The interface now focuses solely on strategy management (registration
and retrieval), while authentication is performed directly by Strategy
implementations.

This separation improves performance by eliminating indirection in the
hot path (per-request authentication) and clarifies the single
responsibility of each component: the registry manages strategies,
strategies perform authentication.
Introduces pkg/vmcp/auth/factory to break the circular dependency
between pkg/vmcp/auth and pkg/vmcp/auth/strategies.

The import cycle occurred because:
- auth package needed to import strategies to instantiate them
- strategies package imported auth for Identity and context helpers

The factory package sits at the composition layer and can import both
auth (for interfaces) and strategies (for implementations) without
creating cycles.
Refactors HTTPBackendClient to accept an OutgoingAuthRegistry and
apply authentication strategies to all backend requests via a new
authRoundTripper middleware.

Authentication is now resolved and validated once at client creation
time rather than per-request, improving performance and enabling
early error detection for misconfigurations.

The authRoundTripper clones requests to preserve immutability before
applying authentication, ensuring thread-safety and preventing
unintended side effects.
The CLI backend discoverer now accepts authentication configuration and
applies it to discovered backends during the discovery process.

This change enables per-backend authentication by:
- Adding authConfig parameter to NewCLIBackendDiscoverer constructor
- Implementing resolveAuthConfig() to select backend-specific or default
authentication settings with proper precedence
- Populating Backend.AuthStrategy and Backend.AuthMetadata fields during
backend creation

Authentication configuration follows this precedence:
1. Backend-specific configuration (cfg.Backends[backendID])
2. Default configuration (cfg.Default)
3. No authentication (if neither is configured)

The populated authentication fields are later consumed when converting
Backend instances to BackendTarget for use by the HTTP client's
authRoundTripper.
Finalizes the end-to-end authentication flow by connecting the
authentication factory, backend discoverer, and HTTP client in the
serve command. This enables vMCP proxy to authenticate requests to
downstream MCP servers using configured authentication strategies.

The serve command now:
- Creates outgoing authenticator from configuration using the factory
- Provides authentication config to backend discoverer for setup
- Supplies authenticator to HTTP client for request signing
- Uses factory for incoming authentication middleware (consistency)

This completes the authentication architecture where configuration
flows through the factory to create strategies that are applied by
the client's round tripper to outgoing requests.

Also simplifies redundant type annotation in client variable
declaration for consistency with Go style conventions.
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.
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.
Limit validTypes to strategies actually implemented in this PR:
- unauthenticated
- header_injection

Comment out unimplemented strategies with TODO to add them as they
are implemented in future PRs. This prevents accepting configuration
for strategies that don't exist yet.
Update example configuration files to use only implemented
authentication strategies (unauthenticated and header_injection).

Changes:
- Replace pass_through with unauthenticated in defaults
- Show header_injection example for backends
- Comment out unimplemented strategies (pass_through, token_exchange,
  service_account) with TODOs
- Add clear notes about which strategies are currently implemented

This ensures example configs are valid and can be used immediately
without validation errors.
Rename the misleading 'api_key' field to 'header_value' to better
reflect that this strategy can inject any HTTP header value, not just
API keys. This improves semantic clarity and matches the general-
purpose nature of the strategy.
Add dedicated rawHeaderInjectionAuth struct and update
transformBackendAuthStrategy to properly parse header_injection
configuration from YAML files.

Previously, the header_injection strategy used a generic metadata field
in YAML, but the transform function had no case to handle it, resulting
in empty metadata maps. This follows the established pattern used by
token_exchange and service_account strategies.

The YAML format is now consistent across all strategies:
  backends:
    github:
      type: header_injection
      header_injection:
        header_name: Authorization
        header_value: Bearer xxx
QueryCapabilities was manually creating BackendTarget but omitted
AuthStrategy and AuthMetadata fields, causing all backends to fall
back to unauthenticated strategy during capability queries.

Replace manual struct creation with BackendToTarget() helper to
ensure all fields (including auth) are properly copied from Backend
to BackendTarget.

This bug prevented per-backend authentication from working during
the initial capability discovery phase, even though auth was
correctly configured by the discoverer.
@jhrozek jhrozek force-pushed the vmcp_out_strategies branch from c117b5d to f1dd6a2 Compare November 4, 2025 23:35
@jhrozek jhrozek marked this pull request as ready for review November 4, 2025 23:35
@jhrozek jhrozek merged commit 03302b2 into main Nov 5, 2025
41 checks passed
@jhrozek jhrozek deleted the vmcp_out_strategies branch November 5, 2025 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants