Skip to content

Commit 14bfdf1

Browse files
committed
Add a registry of outgoing auth strategies with a stub of AuthenticateRequest()
Implement OutgoingAuthenticator interface with pluggable authentication strategies for backend MCP server connections. The actual strategies will be implemented in a follow-up commit. Fixes: #2377
1 parent 809f04f commit 14bfdf1

File tree

4 files changed

+627
-0
lines changed

4 files changed

+627
-0
lines changed

pkg/vmcp/auth/auth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package auth
1111

1212
//go:generate mockgen -destination=mocks/mock_token_authenticator.go -package=mocks github.com/stacklok/toolhive/pkg/vmcp/auth TokenAuthenticator
13+
//go:generate mockgen -destination=mocks/mock_strategy.go -package=mocks github.com/stacklok/toolhive/pkg/vmcp/auth Strategy
1314

1415
import (
1516
"context"

pkg/vmcp/auth/mocks/mock_strategy.go

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"sync"
9+
)
10+
11+
// DefaultOutgoingAuthenticator is a thread-safe implementation of OutgoingAuthenticator
12+
// that maintains a registry of authentication strategies.
13+
//
14+
// This authenticator supports dynamic registration of strategies and dispatches
15+
// authentication requests to the appropriate strategy based on the strategy name.
16+
// It uses sync.RWMutex for thread-safety as HTTP servers are inherently concurrent.
17+
//
18+
// Example usage:
19+
//
20+
// auth := NewDefaultOutgoingAuthenticator()
21+
// auth.RegisterStrategy("bearer", NewBearerStrategy())
22+
// err := auth.AuthenticateRequest(ctx, req, "bearer", metadata)
23+
type DefaultOutgoingAuthenticator struct {
24+
strategies map[string]Strategy
25+
mu sync.RWMutex
26+
}
27+
28+
// NewDefaultOutgoingAuthenticator creates a new DefaultOutgoingAuthenticator
29+
// with an empty strategy registry.
30+
//
31+
// Strategies must be registered using RegisterStrategy before they can be used
32+
// for authentication.
33+
func NewDefaultOutgoingAuthenticator() *DefaultOutgoingAuthenticator {
34+
return &DefaultOutgoingAuthenticator{
35+
strategies: make(map[string]Strategy),
36+
}
37+
}
38+
39+
// RegisterStrategy registers a new authentication strategy.
40+
//
41+
// This method is thread-safe and validates that:
42+
// - name is not empty
43+
// - strategy is not nil
44+
// - no strategy is already registered with the same name
45+
//
46+
// Parameters:
47+
// - name: The unique identifier for this strategy
48+
// - strategy: The Strategy implementation to register
49+
//
50+
// Returns an error if validation fails or a strategy with the same name
51+
// already exists.
52+
func (a *DefaultOutgoingAuthenticator) RegisterStrategy(name string, strategy Strategy) error {
53+
if name == "" {
54+
return errors.New("strategy name cannot be empty")
55+
}
56+
if strategy == nil {
57+
return errors.New("strategy cannot be nil")
58+
}
59+
60+
a.mu.Lock()
61+
defer a.mu.Unlock()
62+
63+
if _, exists := a.strategies[name]; exists {
64+
return fmt.Errorf("strategy %q is already registered", name)
65+
}
66+
67+
a.strategies[name] = strategy
68+
return nil
69+
}
70+
71+
// GetStrategy retrieves an authentication strategy by name.
72+
//
73+
// This method is thread-safe for concurrent reads. It returns the strategy
74+
// if found, or an error if no strategy is registered with the given name.
75+
//
76+
// Parameters:
77+
// - name: The identifier of the strategy to retrieve
78+
//
79+
// Returns:
80+
// - Strategy: The registered strategy
81+
// - error: An error if the strategy is not found
82+
func (a *DefaultOutgoingAuthenticator) GetStrategy(name string) (Strategy, error) {
83+
a.mu.RLock()
84+
defer a.mu.RUnlock()
85+
86+
strategy, exists := a.strategies[name]
87+
if !exists {
88+
return nil, fmt.Errorf("strategy %q not found", name)
89+
}
90+
91+
return strategy, nil
92+
}
93+
94+
// AuthenticateRequest adds authentication to an outgoing backend request.
95+
//
96+
// This method retrieves the specified strategy and delegates authentication
97+
// to it. The strategy modifies the request by adding appropriate headers,
98+
// tokens, or other authentication artifacts.
99+
//
100+
// Parameters:
101+
// - ctx: Request context (may contain identity for pass-through auth)
102+
// - req: The HTTP request to authenticate
103+
// - strategyName: The name of the strategy to use
104+
// - metadata: Strategy-specific configuration
105+
//
106+
// Returns an error if:
107+
// - The strategy is not found
108+
// - The strategy's Authenticate method fails
109+
func (a *DefaultOutgoingAuthenticator) AuthenticateRequest(
110+
ctx context.Context,
111+
req *http.Request,
112+
strategyName string,
113+
metadata map[string]any,
114+
) error {
115+
strategy, err := a.GetStrategy(strategyName)
116+
if err != nil {
117+
return err
118+
}
119+
120+
return strategy.Authenticate(ctx, req, metadata)
121+
}

0 commit comments

Comments
 (0)