Skip to content
16 changes: 11 additions & 5 deletions cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,17 @@ type EmbeddedAuthServerConfig struct {
// +optional
Storage *AuthServerStorageConfig `json:"storage,omitempty"`

// AllowedAudiences is the list of valid resource URIs that tokens can be issued for.
// For an embedded auth server, this can be determined by the servers (MCP or vMCP) it serves.

// ScopesSupported is the list of OAuth 2.0 scopes that this authorization server supports.
// For an embedded auth server, this can be derived from the server's (MCP or vMCP) OIDC configuration.
// BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
// included in every client registration. The embedded auth server unions these
// scopes into the registered set returned by RFC 7591 Dynamic Client
// Registration, so a client that narrows the `scope` field at /oauth/register
// can still request the baseline scopes at /oauth/authorize. All values must
// be present in the upstream-derived scopesSupported set; the auth server
// fails to start if any value is missing.
// +kubebuilder:validation:MaxItems=10
// +listType=atomic
// +optional
BaselineClientScopes []string `json:"baselineClientScopes,omitempty"`
}

// TokenLifespanConfig holds configuration for token lifetimes.
Expand Down
5 changes: 5 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.

1 change: 1 addition & 0 deletions cmd/thv-operator/pkg/controllerutil/authserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ func BuildAuthServerRunConfig(
AuthorizationEndpointBaseURL: authConfig.AuthorizationEndpointBaseURL,
AllowedAudiences: allowedAudiences,
ScopesSupported: scopesSupported,
BaselineClientScopes: authConfig.BaselineClientScopes,
}

// Build signing key configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,20 @@ spec:
Must be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.
pattern: ^https?://[^\s?#]+[^/\s?#]$
type: string
baselineClientScopes:
description: |-
BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
items:
type: string
maxItems: 10
type: array
x-kubernetes-list-type: atomic
hmacSecretRefs:
description: |-
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
Expand Down Expand Up @@ -1385,6 +1399,20 @@ spec:
Must be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.
pattern: ^https?://[^\s?#]+[^/\s?#]$
type: string
baselineClientScopes:
description: |-
BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
items:
type: string
maxItems: 10
type: array
x-kubernetes-list-type: atomic
hmacSecretRefs:
description: |-
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ spec:
Must be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.
pattern: ^https?://[^\s?#]+[^/\s?#]$
type: string
baselineClientScopes:
description: |-
BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
items:
type: string
maxItems: 10
type: array
x-kubernetes-list-type: atomic
hmacSecretRefs:
description: |-
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
Expand Down Expand Up @@ -2723,6 +2737,20 @@ spec:
Must be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.
pattern: ^https?://[^\s?#]+[^/\s?#]$
type: string
baselineClientScopes:
description: |-
BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
items:
type: string
maxItems: 10
type: array
x-kubernetes-list-type: atomic
hmacSecretRefs:
description: |-
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ spec:
Must be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.
pattern: ^https?://[^\s?#]+[^/\s?#]$
type: string
baselineClientScopes:
description: |-
BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
items:
type: string
maxItems: 10
type: array
x-kubernetes-list-type: atomic
hmacSecretRefs:
description: |-
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
Expand Down Expand Up @@ -1388,6 +1402,20 @@ spec:
Must be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.
pattern: ^https?://[^\s?#]+[^/\s?#]$
type: string
baselineClientScopes:
description: |-
BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
items:
type: string
maxItems: 10
type: array
x-kubernetes-list-type: atomic
hmacSecretRefs:
description: |-
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ spec:
Must be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.
pattern: ^https?://[^\s?#]+[^/\s?#]$
type: string
baselineClientScopes:
description: |-
BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
items:
type: string
maxItems: 10
type: array
x-kubernetes-list-type: atomic
hmacSecretRefs:
description: |-
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
Expand Down Expand Up @@ -2726,6 +2740,20 @@ spec:
Must be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.
pattern: ^https?://[^\s?#]+[^/\s?#]$
type: string
baselineClientScopes:
description: |-
BaselineClientScopes is a baseline set of OAuth 2.0 scopes guaranteed to be
included in every client registration. The embedded auth server unions these
scopes into the registered set returned by RFC 7591 Dynamic Client
Registration, so a client that narrows the `scope` field at /oauth/register
can still request the baseline scopes at /oauth/authorize. All values must
be present in the upstream-derived scopesSupported set; the auth server
fails to start if any value is missing.
items:
type: string
maxItems: 10
type: array
x-kubernetes-list-type: atomic
hmacSecretRefs:
description: |-
HMACSecretRefs references Kubernetes Secrets containing symmetric secrets for signing
Expand Down
1 change: 1 addition & 0 deletions docs/operator/crd-api.md

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

45 changes: 45 additions & 0 deletions pkg/authserver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ type RunConfig struct {
// If empty, defaults to registration.DefaultScopes (["openid", "profile", "email", "offline_access"]).
ScopesSupported []string `json:"scopes_supported,omitempty" yaml:"scopes_supported,omitempty"`

// BaselineClientScopes is a baseline set of OAuth 2.0 scopes unioned into every
// DCR registration. All values must appear in ScopesSupported; the auth server
// rejects this RunConfig at startup otherwise. Empty means current behavior is
// preserved (registered scope = client-requested or DefaultScopes if empty).
// When BaselineClientScopes is non-empty, ScopesSupported must be set
// explicitly: validation runs before defaults are applied, so an empty
// ScopesSupported won't be substituted with registration.DefaultScopes for
// purposes of the subset check.
//nolint:lll // field tags require full JSON+YAML names
BaselineClientScopes []string `json:"baseline_client_scopes,omitempty" yaml:"baseline_client_scopes,omitempty"`

// AllowedAudiences is the list of valid resource URIs that tokens can be issued for.
// Per RFC 8707, the "resource" parameter in authorization and token requests is
// validated against this list. Required for MCP compliance.
Expand All @@ -81,6 +92,23 @@ type RunConfig struct {
Storage *storage.RunConfig `json:"storage,omitempty" yaml:"storage,omitempty"`
}

// Validate checks that the on-disk RunConfig is internally consistent. Called
// by the runner before resolving secrets and building the runtime Config; it
// catches operator-supplied misconfiguration early so server startup fails
// loudly instead of degrading silently at runtime.
func (c *RunConfig) Validate() error {
return c.validateBaselineClientScopes()
}

// validateBaselineClientScopes ensures every entry in BaselineClientScopes is
// also present in ScopesSupported. If a baseline scope is not advertised by
// ScopesSupported, the embedded DCR handler would later try to register a
// client with a scope the server does not support, which fosite rejects at
// /oauth/authorize with invalid_scope.
func (c *RunConfig) validateBaselineClientScopes() error {
return registration.ValidateScopeSubset(c.BaselineClientScopes, c.ScopesSupported, "baseline_client_scopes")
}

// SigningKeyRunConfig configures where to load signing keys from.
// Keys are loaded from PEM-encoded files on disk (typically mounted from secrets).
type SigningKeyRunConfig struct {
Expand Down Expand Up @@ -456,6 +484,17 @@ type Config struct {
// /.well-known/oauth-authorization-server discovery endpoints.
ScopesSupported []string

// BaselineClientScopes is a baseline set of OAuth 2.0 scopes the embedded
// DCR handler unions into every newly registered client's scope set. Empty
// means current behavior is preserved (DCR registers exactly what the client
// requested, or registration.DefaultScopes if the client requested none).
// All entries must also be present in ScopesSupported.
// When BaselineClientScopes is non-empty, ScopesSupported must be set
// explicitly: applyDefaults rejects an empty ScopesSupported in that case
// rather than silently substituting DefaultScopes, which would otherwise
// mask the operator's intent.
BaselineClientScopes []string

// AllowedAudiences is the list of valid resource URIs that tokens can be issued for.
// Per RFC 8707, the "resource" parameter in authorization and token requests is
// validated against this list. MCP clients are required to include the resource
Expand Down Expand Up @@ -725,6 +764,12 @@ func (c *Config) applyDefaults() error {
"warning", "JWTs will be invalid after restart")
}
if len(c.ScopesSupported) == 0 {
if len(c.BaselineClientScopes) > 0 {
return fmt.Errorf(
"baseline_client_scopes is non-empty but scopes_supported is empty: " +
"scopes_supported must be set explicitly when baseline_client_scopes is configured",
)
}
c.ScopesSupported = registration.DefaultScopes
slog.Debug("applied default scopes_supported", "scopes", c.ScopesSupported)
}
Expand Down
Loading
Loading