Skip to content
Draft
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
18 changes: 16 additions & 2 deletions cmd/thv-operator/controllers/virtualmcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ func (r *VirtualMCPServerReconciler) applyStatusUpdates(
}

// runValidations runs all pre-reconciliation validations (PodTemplateSpec, GroupRef,
// CompositeToolRefs, EmbeddingServerRef).
// CompositeToolRefs, EmbeddingServerRef, AuthServerConfig).
// Returns (true, nil) to continue reconciliation.
// Returns (false, nil) for spec validation errors that should NOT trigger requeue
// (user must fix the spec; next reconciliation is triggered by spec changes).
Expand Down Expand Up @@ -321,6 +321,16 @@ func (r *VirtualMCPServerReconciler) runValidations(
}
}

// Validate inline AuthServerConfig (when specified).
// Structural validation is handled by CRD Validate() — just set the success condition.
if vmcp.Spec.AuthServerConfig != nil {
statusManager.SetAuthServerConfigValidatedCondition(
mcpv1alpha1.ConditionReasonAuthServerConfigValid,
"AuthServerConfig is valid",
metav1.ConditionTrue,
)
}

return true, nil
}

Expand Down Expand Up @@ -2243,12 +2253,16 @@ func (*VirtualMCPServerReconciler) vmcpReferencesToolConfig(vmcp *mcpv1alpha1.Vi
}

// vmcpReferencesExternalAuthConfig checks if a VirtualMCPServer references the given MCPExternalAuthConfig.
// It checks both inline references (in outgoingAuth spec) and discovered references (via MCPServers in the group).
// It checks authServerConfigRef, inline references (in outgoingAuth spec), and discovered references
// (via MCPServers in the group).
func (r *VirtualMCPServerReconciler) vmcpReferencesExternalAuthConfig(
ctx context.Context,
vmcp *mcpv1alpha1.VirtualMCPServer,
authConfigName string,
) bool {
// Note: AuthServerConfig is inline (not a ref), so it doesn't reference
// MCPExternalAuthConfig resources. Only outgoing auth refs are checked here.

if vmcp.Spec.OutgoingAuth == nil {
return false
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/thv-operator/pkg/virtualmcpserverstatus/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ func (s *StatusCollector) SetEmbeddingServerReadyCondition(reason, message strin
s.SetCondition(mcpv1alpha1.ConditionTypeEmbeddingServerReady, reason, message, status)
}

// SetAuthServerConfigValidatedCondition sets the AuthServerConfigValidated condition.
func (s *StatusCollector) SetAuthServerConfigValidatedCondition(reason, message string, status metav1.ConditionStatus) {
s.SetCondition(mcpv1alpha1.ConditionTypeAuthServerConfigValidated, reason, message, status)
}

// SetDiscoveredBackends sets the discovered backends list to be updated.
func (s *StatusCollector) SetDiscoveredBackends(backends []mcpv1alpha1.DiscoveredBackend) {
s.discoveredBackends = backends
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cmd/thv-operator/pkg/virtualmcpserverstatus/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type StatusManager interface {
// SetEmbeddingServerReadyCondition sets the EmbeddingServerReady condition
SetEmbeddingServerReadyCondition(reason, message string, status metav1.ConditionStatus)

// SetAuthServerConfigValidatedCondition sets the AuthServerConfigValidated condition
SetAuthServerConfigValidatedCondition(reason, message string, status metav1.ConditionStatus)

// SetDiscoveredBackends sets the discovered backends list
SetDiscoveredBackends(backends []mcpv1alpha1.DiscoveredBackend)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,13 @@ spec:
items:
type: string
type: array
subjectProviderName:
description: |-
SubjectProviderName is the upstream provider name whose token is used as the
RFC 8693 subject token instead of identity.Token when performing token exchange.
When set, the strategy looks up the provider's access token from
identity.UpstreamTokens rather than using the incoming bearer token.
type: string
subjectTokenType:
description: |-
SubjectTokenType is the token type of the incoming subject token.
Expand All @@ -1419,8 +1426,21 @@ spec:
type: object
type:
description: 'Type is the auth strategy: "unauthenticated",
"header_injection", "token_exchange"'
"header_injection", "token_exchange", "upstream_inject"'
type: string
upstreamInject:
description: |-
UpstreamInject contains configuration for upstream inject auth strategy.
Used when Type = "upstream_inject".
properties:
providerName:
description: |-
ProviderName is the name of the upstream provider configured in the
embedded authorization server. Must match an entry in AuthServer.Upstreams.
type: string
required:
- providerName
type: object
required:
- type
type: object
Expand Down Expand Up @@ -1481,6 +1501,13 @@ spec:
items:
type: string
type: array
subjectProviderName:
description: |-
SubjectProviderName is the upstream provider name whose token is used as the
RFC 8693 subject token instead of identity.Token when performing token exchange.
When set, the strategy looks up the provider's access token from
identity.UpstreamTokens rather than using the incoming bearer token.
type: string
subjectTokenType:
description: |-
SubjectTokenType is the token type of the incoming subject token.
Expand All @@ -1495,8 +1522,21 @@ spec:
type: object
type:
description: 'Type is the auth strategy: "unauthenticated",
"header_injection", "token_exchange"'
"header_injection", "token_exchange", "upstream_inject"'
type: string
upstreamInject:
description: |-
UpstreamInject contains configuration for upstream inject auth strategy.
Used when Type = "upstream_inject".
properties:
providerName:
description: |-
ProviderName is the name of the upstream provider configured in the
embedded authorization server. Must match an entry in AuthServer.Upstreams.
type: string
required:
- providerName
type: object
required:
- type
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,13 @@ spec:
items:
type: string
type: array
subjectProviderName:
description: |-
SubjectProviderName is the upstream provider name whose token is used as the
RFC 8693 subject token instead of identity.Token when performing token exchange.
When set, the strategy looks up the provider's access token from
identity.UpstreamTokens rather than using the incoming bearer token.
type: string
subjectTokenType:
description: |-
SubjectTokenType is the token type of the incoming subject token.
Expand All @@ -1422,8 +1429,21 @@ spec:
type: object
type:
description: 'Type is the auth strategy: "unauthenticated",
"header_injection", "token_exchange"'
"header_injection", "token_exchange", "upstream_inject"'
type: string
upstreamInject:
description: |-
UpstreamInject contains configuration for upstream inject auth strategy.
Used when Type = "upstream_inject".
properties:
providerName:
description: |-
ProviderName is the name of the upstream provider configured in the
embedded authorization server. Must match an entry in AuthServer.Upstreams.
type: string
required:
- providerName
type: object
required:
- type
type: object
Expand Down Expand Up @@ -1484,6 +1504,13 @@ spec:
items:
type: string
type: array
subjectProviderName:
description: |-
SubjectProviderName is the upstream provider name whose token is used as the
RFC 8693 subject token instead of identity.Token when performing token exchange.
When set, the strategy looks up the provider's access token from
identity.UpstreamTokens rather than using the incoming bearer token.
type: string
subjectTokenType:
description: |-
SubjectTokenType is the token type of the incoming subject token.
Expand All @@ -1498,8 +1525,21 @@ spec:
type: object
type:
description: 'Type is the auth strategy: "unauthenticated",
"header_injection", "token_exchange"'
"header_injection", "token_exchange", "upstream_inject"'
type: string
upstreamInject:
description: |-
UpstreamInject contains configuration for upstream inject auth strategy.
Used when Type = "upstream_inject".
properties:
providerName:
description: |-
ProviderName is the name of the upstream provider configured in the
embedded authorization server. Must match an entry in AuthServer.Upstreams.
type: string
required:
- providerName
type: object
required:
- type
type: object
Expand Down
22 changes: 21 additions & 1 deletion docs/operator/crd-api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions pkg/authserver/runner/embeddedauthserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ func (e *EmbeddedAuthServer) UpstreamTokenRefresher() storage.UpstreamTokenRefre
return e.server.UpstreamTokenRefresher()
}

// RegisterHandlers registers the authorization server's HTTP routes on the given mux.
// The AS owns its route paths — adding new endpoints in the future does not require
// changes to the caller.
func (e *EmbeddedAuthServer) RegisterHandlers(mux *http.ServeMux) {
handler := e.Handler()
mux.Handle("/.well-known/openid-configuration", handler)
mux.Handle("/.well-known/oauth-authorization-server", handler)
mux.Handle("/.well-known/jwks.json", handler)
mux.Handle("/oauth/", handler)
}

// createKeyProvider creates a KeyProvider from SigningKeyRunConfig.
// Returns a GeneratingProvider if config is nil or empty (development mode).
func createKeyProvider(cfg *authserver.SigningKeyRunConfig) (keys.KeyProvider, error) {
Expand Down
37 changes: 36 additions & 1 deletion pkg/vmcp/auth/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@
// Types defined here include:
// - Strategy type constants (StrategyTypeUnauthenticated, etc.)
// - Backend auth configuration structs (BackendAuthStrategy, etc.)
// - Sentinel errors for strategy error handling
package types

import "errors"

// ErrUpstreamTokenNotFound is returned when an upstream IDP token is not found
// in identity.UpstreamTokens for the requested provider. Callers should check
// with errors.Is() and wrap with %w at the call site.
var ErrUpstreamTokenNotFound = errors.New("upstream token not found")

// Strategy type identifiers used to identify authentication strategies.
const (
// StrategyTypeUnauthenticated identifies the unauthenticated strategy.
Expand All @@ -27,6 +35,11 @@ const (
// This strategy exchanges an incoming token for a new token to use
// when authenticating to the backend service.
StrategyTypeTokenExchange = "token_exchange"

// StrategyTypeUpstreamInject identifies the upstream inject strategy.
// This strategy injects an upstream IDP token obtained by the embedded
// authorization server into requests to the backend service.
StrategyTypeUpstreamInject = "upstream_inject"
)

// BackendAuthStrategy defines how to authenticate to a specific backend.
Expand All @@ -36,7 +49,7 @@ const (
// +kubebuilder:object:generate=true
// +gendoc
type BackendAuthStrategy struct {
// Type is the auth strategy: "unauthenticated", "header_injection", "token_exchange"
// Type is the auth strategy: "unauthenticated", "header_injection", "token_exchange", "upstream_inject"
Type string `json:"type" yaml:"type"`

// HeaderInjection contains configuration for header injection auth strategy.
Expand All @@ -46,6 +59,10 @@ type BackendAuthStrategy struct {
// TokenExchange contains configuration for token exchange auth strategy.
// Used when Type = "token_exchange".
TokenExchange *TokenExchangeConfig `json:"tokenExchange,omitempty" yaml:"tokenExchange,omitempty"`

// UpstreamInject contains configuration for upstream inject auth strategy.
// Used when Type = "upstream_inject".
UpstreamInject *UpstreamInjectConfig `json:"upstreamInject,omitempty" yaml:"upstreamInject,omitempty"`
}

// HeaderInjectionConfig configures the header injection auth strategy.
Expand Down Expand Up @@ -94,4 +111,22 @@ type TokenExchangeConfig struct {
// SubjectTokenType is the token type of the incoming subject token.
// Defaults to "urn:ietf:params:oauth:token-type:access_token" if not specified.
SubjectTokenType string `json:"subjectTokenType,omitempty" yaml:"subjectTokenType,omitempty"`

// SubjectProviderName is the upstream provider name whose token is used as the
// RFC 8693 subject token instead of identity.Token when performing token exchange.
// When set, the strategy looks up the provider's access token from
// identity.UpstreamTokens rather than using the incoming bearer token.
// +optional
SubjectProviderName string `json:"subjectProviderName,omitempty" yaml:"subjectProviderName,omitempty"`
}

// UpstreamInjectConfig configures the upstream inject auth strategy.
// This strategy uses the embedded authorization server to obtain and inject
// upstream IDP tokens into backend requests.
// +kubebuilder:object:generate=true
// +gendoc
type UpstreamInjectConfig struct {
// ProviderName is the name of the upstream provider configured in the
// embedded authorization server. Must match an entry in AuthServer.Upstreams.
ProviderName string `json:"providerName" yaml:"providerName"`
}
Loading
Loading