Skip to content
Open
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
52 changes: 51 additions & 1 deletion cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,18 @@ type OIDCUpstreamConfig struct {
// +optional
AdditionalAuthorizationParams map[string]string `json:"additionalAuthorizationParams,omitempty"`

// AdditionalTokenParams are extra form-body parameters to include in
// token requests (authorization code exchange and refresh) sent to the
// upstream provider's token endpoint.
// This is useful for providers that enforce RFC 8707 resource indicators
// on token requests, where the resource parameter must accompany the code
// exchange and refresh, not only the authorization request.
// Framework-managed parameters (grant_type, code, redirect_uri, client_id,
// client_secret, code_verifier, refresh_token, scope) are not allowed.
// +kubebuilder:validation:MaxProperties=16
// +optional
AdditionalTokenParams map[string]string `json:"additionalTokenParams,omitempty"`

// SubjectClaim names the validated ID-token claim to use as the upstream
// subject. Defaults to "sub" when empty. Set it for IdPs where "sub" isn't
// stable per user — e.g. Entra/Azure AD, whose "sub" rotates per application
Expand Down Expand Up @@ -1128,6 +1140,18 @@ type OAuth2UpstreamConfig struct {
// +optional
AdditionalAuthorizationParams map[string]string `json:"additionalAuthorizationParams,omitempty"`

// AdditionalTokenParams are extra form-body parameters to include in
// token requests (authorization code exchange and refresh) sent to the
// upstream provider's token endpoint.
// This is useful for providers that enforce RFC 8707 resource indicators
// on token requests, where the resource parameter must accompany the code
// exchange and refresh, not only the authorization request.
// Framework-managed parameters (grant_type, code, redirect_uri, client_id,
// client_secret, code_verifier, refresh_token, scope) are not allowed.
// +kubebuilder:validation:MaxProperties=16
// +optional
AdditionalTokenParams map[string]string `json:"additionalTokenParams,omitempty"`

// InsecureAllowHTTP permits plain-HTTP authorization and token endpoint URLs
// for this upstream. Only for in-cluster development environments (e.g. an
// OAuth2 provider served over HTTP in a kind cluster) where TLS is not
Expand Down Expand Up @@ -2070,7 +2094,12 @@ func (*MCPExternalAuthConfig) validateUpstreamProvider(index int, provider *Upst
}

// Validate additionalAuthorizationParams does not contain reserved keys
return ValidateAdditionalAuthorizationParams(prefix, provider.AdditionalAuthorizationParams())
if err := ValidateAdditionalAuthorizationParams(prefix, provider.AdditionalAuthorizationParams()); err != nil {
return err
}

// Validate additionalTokenParams does not contain reserved keys
return ValidateAdditionalTokenParams(prefix, provider.AdditionalTokenParams())
}

// Length caps for DCR-related string fields. Mirror the
Expand Down Expand Up @@ -2165,6 +2194,18 @@ func (p *UpstreamProviderConfig) AdditionalAuthorizationParams() map[string]stri
return nil
}

// AdditionalTokenParams returns the additional token-request parameters
// from whichever upstream config is set, or nil if none.
func (p *UpstreamProviderConfig) AdditionalTokenParams() map[string]string {
if p.OIDCConfig != nil {
return p.OIDCConfig.AdditionalTokenParams
}
if p.OAuth2Config != nil {
return p.OAuth2Config.AdditionalTokenParams
}
return nil
}

// SyntheticIdentityUpstreams returns the names of OAuth2 upstreams running
// in synthesis mode (neither userInfo nor identityFromToken configured),
// sorted lexically for deterministic condition messages. OIDC upstreams are
Expand Down Expand Up @@ -2199,6 +2240,15 @@ func ValidateAdditionalAuthorizationParams(prefix string, params map[string]stri
return nil
}

// ValidateAdditionalTokenParams checks that no reserved OAuth2 token-request
// parameters are present in the additional token params map.
func ValidateAdditionalTokenParams(prefix string, params map[string]string) error {
if err := oauthparams.ValidateTokenParams(params); err != nil {
return fmt.Errorf("%s.additionalTokenParams: %w", prefix, err)
}
return nil
}

// validateAWSSts validates awsSts type configuration.
// This performs complex business logic validation that CEL cannot express.
func (r *MCPExternalAuthConfig) validateAWSSts() error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,72 @@ func TestMCPExternalAuthConfig_validateUpstreamProvider(t *testing.T) {
},
expectErr: false,
},
{
name: "OIDC provider with valid additionalTokenParams",
provider: UpstreamProviderConfig{
Name: "nominal",
Type: UpstreamProviderTypeOIDC,
OIDCConfig: &OIDCUpstreamConfig{
IssuerURL: "https://idp.example.com",
ClientID: "client-id",
AdditionalTokenParams: map[string]string{
"resource": "https://api.example.com/mcp",
},
},
},
expectErr: false,
},
{
name: "OAuth2 provider with valid additionalTokenParams",
provider: UpstreamProviderConfig{
Name: "nominal",
Type: UpstreamProviderTypeOAuth2,
OAuth2Config: &OAuth2UpstreamConfig{
AuthorizationEndpoint: "https://oauth.example.com/authorize",
TokenEndpoint: "https://oauth.example.com/token",
ClientID: "client-id",
UserInfo: &UserInfoConfig{EndpointURL: "https://oauth.example.com/userinfo"},
AdditionalTokenParams: map[string]string{
"resource": "https://api.example.com/mcp",
},
},
},
expectErr: false,
},
{
name: "OAuth2 provider with reserved token param grant_type",
provider: UpstreamProviderConfig{
Name: "custom",
Type: UpstreamProviderTypeOAuth2,
OAuth2Config: &OAuth2UpstreamConfig{
AuthorizationEndpoint: "https://oauth.example.com/authorize",
TokenEndpoint: "https://oauth.example.com/token",
ClientID: "client-id",
UserInfo: &UserInfoConfig{EndpointURL: "https://oauth.example.com/userinfo"},
AdditionalTokenParams: map[string]string{
"grant_type": "password",
},
},
},
expectErr: true,
errMsg: "reserved token parameter \"grant_type\" is managed by the framework",
},
{
name: "OIDC provider with reserved token param refresh_token",
provider: UpstreamProviderConfig{
Name: "custom",
Type: UpstreamProviderTypeOIDC,
OIDCConfig: &OIDCUpstreamConfig{
IssuerURL: "https://idp.example.com",
ClientID: "client-id",
AdditionalTokenParams: map[string]string{
"refresh_token": "override-attempt",
},
},
},
expectErr: true,
errMsg: "reserved token parameter \"refresh_token\" is managed by the framework",
},
{
name: "OAuth2 provider with valid DCRConfig (discoveryUrl only)",
provider: UpstreamProviderConfig{
Expand Down
14 changes: 14 additions & 0 deletions cmd/thv-operator/api/v1beta1/zz_generated.deepcopy.go

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

9 changes: 7 additions & 2 deletions cmd/thv-operator/controllers/virtualmcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,11 +610,16 @@ func (*VirtualMCPServerReconciler) validateAuthServerConfig(
return stderrors.New(message)
}

// Validate additionalAuthorizationParams on each upstream provider
// Validate additionalAuthorizationParams / additionalTokenParams on each
// upstream provider
for i := range cfg.UpstreamProviders {
prefix := fmt.Sprintf("spec.authServerConfig.upstreamProviders[%d]", i)
params := cfg.UpstreamProviders[i].AdditionalAuthorizationParams()
if err := mcpv1beta1.ValidateAdditionalAuthorizationParams(prefix, params); err != nil {
err := mcpv1beta1.ValidateAdditionalAuthorizationParams(prefix, params)
if err == nil {
err = mcpv1beta1.ValidateAdditionalTokenParams(prefix, cfg.UpstreamProviders[i].AdditionalTokenParams())
}
if err != nil {
message := err.Error()
statusManager.SetPhase(mcpv1beta1.VirtualMCPServerPhaseFailed)
statusManager.SetMessage(message)
Expand Down
2 changes: 2 additions & 0 deletions cmd/thv-operator/pkg/controllerutil/authserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,7 @@ func buildOIDCUpstreamRunConfig(
RedirectURI: redirectURI,
Scopes: cfg.Scopes,
AdditionalAuthorizationParams: cfg.AdditionalAuthorizationParams,
AdditionalTokenParams: cfg.AdditionalTokenParams,
SubjectClaim: cfg.SubjectClaim,
}
if cfg.ClientSecretRef != nil {
Expand Down Expand Up @@ -1031,6 +1032,7 @@ func buildOAuth2UpstreamRunConfig(
RedirectURI: redirectURI,
Scopes: cfg.Scopes,
AdditionalAuthorizationParams: cfg.AdditionalAuthorizationParams,
AdditionalTokenParams: cfg.AdditionalTokenParams,
}
if cfg.ClientSecretRef != nil {
runConfig.ClientSecretEnvVar = clientSecretEnvVar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,20 @@ spec:
scope, state, code_challenge, code_challenge_method, nonce) are not allowed.
maxProperties: 16
type: object
additionalTokenParams:
additionalProperties:
type: string
description: |-
AdditionalTokenParams are extra form-body parameters to include in
token requests (authorization code exchange and refresh) sent to the
upstream provider's token endpoint.
This is useful for providers that enforce RFC 8707 resource indicators
on token requests, where the resource parameter must accompany the code
exchange and refresh, not only the authorization request.
Framework-managed parameters (grant_type, code, redirect_uri, client_id,
client_secret, code_verifier, refresh_token, scope) are not allowed.
maxProperties: 16
type: object
allowPrivateIPs:
description: |-
AllowPrivateIPs permits the upstream provider's HTTP client to connect to
Expand Down Expand Up @@ -1339,6 +1353,20 @@ spec:
scope, state, code_challenge, code_challenge_method, nonce) are not allowed.
maxProperties: 16
type: object
additionalTokenParams:
additionalProperties:
type: string
description: |-
AdditionalTokenParams are extra form-body parameters to include in
token requests (authorization code exchange and refresh) sent to the
upstream provider's token endpoint.
This is useful for providers that enforce RFC 8707 resource indicators
on token requests, where the resource parameter must accompany the code
exchange and refresh, not only the authorization request.
Framework-managed parameters (grant_type, code, redirect_uri, client_id,
client_secret, code_verifier, refresh_token, scope) are not allowed.
maxProperties: 16
type: object
clientId:
description: ClientID is the OAuth 2.0 client identifier
registered with the upstream IdP.
Expand Down Expand Up @@ -2996,6 +3024,20 @@ spec:
scope, state, code_challenge, code_challenge_method, nonce) are not allowed.
maxProperties: 16
type: object
additionalTokenParams:
additionalProperties:
type: string
description: |-
AdditionalTokenParams are extra form-body parameters to include in
token requests (authorization code exchange and refresh) sent to the
upstream provider's token endpoint.
This is useful for providers that enforce RFC 8707 resource indicators
on token requests, where the resource parameter must accompany the code
exchange and refresh, not only the authorization request.
Framework-managed parameters (grant_type, code, redirect_uri, client_id,
client_secret, code_verifier, refresh_token, scope) are not allowed.
maxProperties: 16
type: object
allowPrivateIPs:
description: |-
AllowPrivateIPs permits the upstream provider's HTTP client to connect to
Expand Down Expand Up @@ -3312,6 +3354,20 @@ spec:
scope, state, code_challenge, code_challenge_method, nonce) are not allowed.
maxProperties: 16
type: object
additionalTokenParams:
additionalProperties:
type: string
description: |-
AdditionalTokenParams are extra form-body parameters to include in
token requests (authorization code exchange and refresh) sent to the
upstream provider's token endpoint.
This is useful for providers that enforce RFC 8707 resource indicators
on token requests, where the resource parameter must accompany the code
exchange and refresh, not only the authorization request.
Framework-managed parameters (grant_type, code, redirect_uri, client_id,
client_secret, code_verifier, refresh_token, scope) are not allowed.
maxProperties: 16
type: object
clientId:
description: ClientID is the OAuth 2.0 client identifier
registered with the upstream IdP.
Expand Down
Loading
Loading