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
49 changes: 49 additions & 0 deletions cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,22 @@ const (
UpstreamProviderTypeOAuth2 UpstreamProviderType = "oauth2"
)

// UpstreamLoginPolicy controls whether an upstream provider participates in
// every login authorization chain.
type UpstreamLoginPolicy string

const (
// UpstreamLoginPolicyRequired walks the provider on every login
// authorization chain (the default, and the behavior before this field
// existed).
UpstreamLoginPolicyRequired UpstreamLoginPolicy = "required"

// UpstreamLoginPolicyOnDemand excludes the provider from the login
// authorization chain, so users are not sent through its consent screen at
// every login.
UpstreamLoginPolicyOnDemand UpstreamLoginPolicy = "onDemand"
)

// UpstreamProviderConfig defines configuration for an upstream Identity Provider.
//
// Exactly one of OIDCConfig or OAuth2Config must be set and must match the
Expand Down Expand Up @@ -589,6 +605,29 @@ type UpstreamProviderConfig struct {
// Required when Type is "oauth2", must be nil when Type is "oidc".
// +optional
OAuth2Config *OAuth2UpstreamConfig `json:"oauth2Config,omitempty"`

// LoginPolicy controls whether this provider participates in every login
// authorization chain. When empty or "required" (the default), the embedded
// auth server walks the provider on every login — the behavior before this
// field existed. When "onDemand", the provider is excluded from the login
// chain: users are not sent through its consent screen at login, and any
// token previously stored for it (for example from an earlier login while
// the provider was still required, or from a future on-demand connect flow)
// continues to be injected and refreshed at MCP-request time.
//
// Note that this field controls only login-time acquisition. There is not
// yet an in-band flow for a user to authorize an onDemand provider after
// login; a request to a backend that needs a token the user never granted
// fails until such a flow exists.
//
// Must not be "onDemand" on the first entry of UpstreamProviders: the first
// provider anchors the chain and resolves the user's identity, so it can
// never be filtered out. Only meaningful when multiple upstream providers
// are configured (VirtualMCPServer); on single-upstream resources the only
// provider is the first, where "onDemand" is rejected.
// +kubebuilder:validation:Enum=required;onDemand
// +optional
LoginPolicy UpstreamLoginPolicy `json:"loginPolicy,omitempty"`
}

// OIDCUpstreamConfig contains configuration for OIDC providers.
Expand Down Expand Up @@ -1609,6 +1648,16 @@ func (r *MCPExternalAuthConfig) validateEmbeddedAuthServer() error {
// (MCPServer, MCPRemoteProxy) enforce single-upstream restrictions;
// VirtualMCPServer allows multiple upstreams.

// The first provider anchors the authorization chain and resolves the
// user's identity, so it can never be excluded from the chain. This is a
// cross-item (positional) rule, which is why it lives here next to the
// duplicate-name check rather than in per-provider CEL.
if cfg.UpstreamProviders[0].LoginPolicy == UpstreamLoginPolicyOnDemand {
return fmt.Errorf("upstreamProviders[0]: loginPolicy %q is not allowed on the first provider; "+
"the first provider anchors the authorization chain and cannot be filtered out",
UpstreamLoginPolicyOnDemand)
}

seen := make(map[string]bool, len(cfg.UpstreamProviders))
for i, provider := range cfg.UpstreamProviders {
if seen[provider.Name] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,66 @@ func TestMCPExternalAuthConfig_Validate(t *testing.T) {
},
expectErr: false,
},
{
name: "embeddedAuthServer with onDemand loginPolicy on non-first provider - valid",
config: &MCPExternalAuthConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "test-embedded-ondemand",
Namespace: "default",
},
Spec: MCPExternalAuthConfigSpec{
Type: ExternalAuthTypeEmbeddedAuthServer,
EmbeddedAuthServer: &EmbeddedAuthServerConfig{
Issuer: "https://auth.example.com",
UpstreamProviders: []UpstreamProviderConfig{
{
Name: "okta",
Type: UpstreamProviderTypeOIDC,
LoginPolicy: UpstreamLoginPolicyRequired,
OIDCConfig: &OIDCUpstreamConfig{IssuerURL: "https://okta.example.com", ClientID: "id1"},
},
{
Name: "github",
Type: UpstreamProviderTypeOIDC,
LoginPolicy: UpstreamLoginPolicyOnDemand,
OIDCConfig: &OIDCUpstreamConfig{IssuerURL: "https://github.com", ClientID: "id2"},
},
},
},
},
},
expectErr: false,
},
{
name: "invalid embeddedAuthServer with onDemand loginPolicy on first provider",
config: &MCPExternalAuthConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "test-embedded-ondemand-first",
Namespace: "default",
},
Spec: MCPExternalAuthConfigSpec{
Type: ExternalAuthTypeEmbeddedAuthServer,
EmbeddedAuthServer: &EmbeddedAuthServerConfig{
Issuer: "https://auth.example.com",
UpstreamProviders: []UpstreamProviderConfig{
{
Name: "okta",
Type: UpstreamProviderTypeOIDC,
LoginPolicy: UpstreamLoginPolicyOnDemand,
OIDCConfig: &OIDCUpstreamConfig{IssuerURL: "https://okta.example.com", ClientID: "id1"},
},
{
Name: "github",
Type: UpstreamProviderTypeOIDC,
OIDCConfig: &OIDCUpstreamConfig{IssuerURL: "https://github.com", ClientID: "id2"},
},
},
},
},
},
expectErr: true,
errMsg: "loginPolicy \"onDemand\" is not allowed on the first provider",
},
{
name: "invalid embeddedAuthServer with no providers",
config: &MCPExternalAuthConfig{
Expand Down
5 changes: 5 additions & 0 deletions cmd/thv-operator/pkg/controllerutil/authserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,11 @@ func buildUpstreamRunConfig(
config := &authserver.UpstreamRunConfig{
Name: provider.Name,
Type: authserver.UpstreamProviderType(provider.Type),
// LoginPolicy passes through verbatim: the CRD enum and the runtime
// constants share the same values ("required"/"onDemand"), admission
// rejects anything else, and CRD-type/vmcp validation rejects
// "onDemand" on the first provider before this conversion runs.
LoginPolicy: authserver.UpstreamLoginPolicy(provider.LoginPolicy),
}

switch provider.Type {
Expand Down
44 changes: 44 additions & 0 deletions cmd/thv-operator/pkg/controllerutil/authserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,50 @@ func TestBuildAuthServerRunConfig(t *testing.T) {
assert.Equal(t, "https://okta.example.com", upstream.OIDCConfig.IssuerURL)
assert.Equal(t, "client-id", upstream.OIDCConfig.ClientID)
assert.Equal(t, []string{"openid", "profile"}, upstream.OIDCConfig.Scopes)
// LoginPolicy was not set on the CRD provider, so it must stay
// empty (the "required" default) in the RunConfig.
assert.Empty(t, upstream.LoginPolicy)
},
},
{
name: "upstream provider loginPolicy propagates to RunConfig",
resourceURL: defaultResourceURL,
authConfig: &mcpv1beta1.EmbeddedAuthServerConfig{
Issuer: "https://auth.example.com",
SigningKeySecretRefs: []mcpv1beta1.SecretKeyRef{
{Name: "signing-key", Key: "private.pem"},
},
HMACSecretRefs: []mcpv1beta1.SecretKeyRef{
{Name: "hmac-secret", Key: "hmac"},
},
UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{
{
Name: "okta",
Type: mcpv1beta1.UpstreamProviderTypeOIDC,
LoginPolicy: mcpv1beta1.UpstreamLoginPolicyRequired,
OIDCConfig: &mcpv1beta1.OIDCUpstreamConfig{
IssuerURL: "https://okta.example.com",
ClientID: "client-id",
},
},
{
Name: "github",
Type: mcpv1beta1.UpstreamProviderTypeOIDC,
LoginPolicy: mcpv1beta1.UpstreamLoginPolicyOnDemand,
OIDCConfig: &mcpv1beta1.OIDCUpstreamConfig{
IssuerURL: "https://github.com",
ClientID: "client-id-2",
},
},
},
},
allowedAudiences: defaultAudiences,
scopesSupported: defaultScopes,
checkFunc: func(t *testing.T, config *authserver.RunConfig) {
t.Helper()
require.Len(t, config.Upstreams, 2)
assert.Equal(t, authserver.UpstreamLoginPolicyRequired, config.Upstreams[0].LoginPolicy)
assert.Equal(t, authserver.UpstreamLoginPolicyOnDemand, config.Upstreams[1].LoginPolicy)
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,31 @@ spec:
rule fails admission instead of silently demanding the OAuth2 shape. When
adding a new type, extend both this rule and validateUpstreamProvider.
properties:
loginPolicy:
description: |-
LoginPolicy controls whether this provider participates in every login
authorization chain. When empty or "required" (the default), the embedded
auth server walks the provider on every login — the behavior before this
field existed. When "onDemand", the provider is excluded from the login
chain: users are not sent through its consent screen at login, and any
token previously stored for it (for example from an earlier login while
the provider was still required, or from a future on-demand connect flow)
continues to be injected and refreshed at MCP-request time.

Note that this field controls only login-time acquisition. There is not
yet an in-band flow for a user to authorize an onDemand provider after
login; a request to a backend that needs a token the user never granted
fails until such a flow exists.

Must not be "onDemand" on the first entry of UpstreamProviders: the first
provider anchors the chain and resolves the user's identity, so it can
never be filtered out. Only meaningful when multiple upstream providers
are configured (VirtualMCPServer); on single-upstream resources the only
provider is the first, where "onDemand" is rejected.
enum:
- required
- onDemand
type: string
name:
description: |-
Name uniquely identifies this upstream provider.
Expand Down Expand Up @@ -2221,6 +2246,31 @@ spec:
rule fails admission instead of silently demanding the OAuth2 shape. When
adding a new type, extend both this rule and validateUpstreamProvider.
properties:
loginPolicy:
description: |-
LoginPolicy controls whether this provider participates in every login
authorization chain. When empty or "required" (the default), the embedded
auth server walks the provider on every login — the behavior before this
field existed. When "onDemand", the provider is excluded from the login
chain: users are not sent through its consent screen at login, and any
token previously stored for it (for example from an earlier login while
the provider was still required, or from a future on-demand connect flow)
continues to be injected and refreshed at MCP-request time.

Note that this field controls only login-time acquisition. There is not
yet an in-band flow for a user to authorize an onDemand provider after
login; a request to a backend that needs a token the user never granted
fails until such a flow exists.

Must not be "onDemand" on the first entry of UpstreamProviders: the first
provider anchors the chain and resolves the user's identity, so it can
never be filtered out. Only meaningful when multiple upstream providers
are configured (VirtualMCPServer); on single-upstream resources the only
provider is the first, where "onDemand" is rejected.
enum:
- required
- onDemand
type: string
name:
description: |-
Name uniquely identifies this upstream provider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,31 @@ spec:
rule fails admission instead of silently demanding the OAuth2 shape. When
adding a new type, extend both this rule and validateUpstreamProvider.
properties:
loginPolicy:
description: |-
LoginPolicy controls whether this provider participates in every login
authorization chain. When empty or "required" (the default), the embedded
auth server walks the provider on every login — the behavior before this
field existed. When "onDemand", the provider is excluded from the login
chain: users are not sent through its consent screen at login, and any
token previously stored for it (for example from an earlier login while
the provider was still required, or from a future on-demand connect flow)
continues to be injected and refreshed at MCP-request time.

Note that this field controls only login-time acquisition. There is not
yet an in-band flow for a user to authorize an onDemand provider after
login; a request to a backend that needs a token the user never granted
fails until such a flow exists.

Must not be "onDemand" on the first entry of UpstreamProviders: the first
provider anchors the chain and resolves the user's identity, so it can
never be filtered out. Only meaningful when multiple upstream providers
are configured (VirtualMCPServer); on single-upstream resources the only
provider is the first, where "onDemand" is rejected.
enum:
- required
- onDemand
type: string
name:
description: |-
Name uniquely identifies this upstream provider.
Expand Down Expand Up @@ -3924,6 +3949,31 @@ spec:
rule fails admission instead of silently demanding the OAuth2 shape. When
adding a new type, extend both this rule and validateUpstreamProvider.
properties:
loginPolicy:
description: |-
LoginPolicy controls whether this provider participates in every login
authorization chain. When empty or "required" (the default), the embedded
auth server walks the provider on every login — the behavior before this
field existed. When "onDemand", the provider is excluded from the login
chain: users are not sent through its consent screen at login, and any
token previously stored for it (for example from an earlier login while
the provider was still required, or from a future on-demand connect flow)
continues to be injected and refreshed at MCP-request time.

Note that this field controls only login-time acquisition. There is not
yet an in-band flow for a user to authorize an onDemand provider after
login; a request to a backend that needs a token the user never granted
fails until such a flow exists.

Must not be "onDemand" on the first entry of UpstreamProviders: the first
provider anchors the chain and resolves the user's identity, so it can
never be filtered out. Only meaningful when multiple upstream providers
are configured (VirtualMCPServer); on single-upstream resources the only
provider is the first, where "onDemand" is rejected.
enum:
- required
- onDemand
type: string
name:
description: |-
Name uniquely identifies this upstream provider.
Expand Down
Loading
Loading