Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 22 additions & 8 deletions cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1879,19 +1879,33 @@ type DCRUpstreamConfig struct {
// Change it to make the proxies register a fresh client at their next
// start instead of reusing the cached one: for example after the
// upstream has dropped the registration, or when a rotation is required.
// At start ToolHive checks the cached client at the upstream's token
// endpoint and registers a new one by itself when the upstream answers
// invalid_client. Where that check cannot tell (an upstream that answers
// a dropped public client like a live one, or a private_key_jwt client),
// the stale client_id is only rejected in the user's browser, and
// changing this key is the remedy. The previous registration's row stays
// in storage, and upstream refresh tokens bound to the previous client_id
// stop refreshing, so affected users sign in again.
// At start ToolHive checks the cached client at the upstream (see
// RegistrationProbe) and registers a new one by itself when the upstream
// rejects it. Where the configured probe cannot tell, the stale client_id
// may only be rejected in the user's browser, and changing this key is
// the remedy. The previous registration's row stays in storage, and
// upstream refresh tokens bound to the previous client_id stop
// refreshing, so affected users sign in again.
// +optional
// +kubebuilder:validation:Pattern=`^[A-Za-z0-9._-]+$`
// +kubebuilder:validation:MaxLength=64
RotationKey string `json:"rotationKey,omitempty"`

// RegistrationProbe selects how ToolHive checks the cached upstream client
// at start. tokenEndpoint (the default) redeems a random authorization
// code with the cached client and reads invalid_client as "registration
// gone"; it decides only for upstreams that authenticate the client
// before validating the grant. authorizationEndpoint sends an
// authorization request for the cached client without following
// redirects and reads a direct 4xx as "gone" (RFC 6749 §4.1.2.1), for
// upstreams whose token endpoint answers invalid_grant for any client_id;
// do not use it behind bot protection that answers 4xx to non-browser
// requests. none skips the check; the registration is then retired only
// through RotationKey.
// +optional
// +kubebuilder:validation:Enum=tokenEndpoint;authorizationEndpoint;none
RegistrationProbe string `json:"registrationProbe,omitempty"`

// InitialAccessTokenRef is an optional reference to a Kubernetes Secret
// carrying an RFC 7591 §3 initial access token. When set, the resolver
// presents the token value as a Bearer credential on the registration
Expand Down
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 @@ -1449,6 +1449,7 @@ func buildDCRUpstreamRunConfig(
DiscoveryURL: dcr.DiscoveryURL,
RegistrationEndpoint: dcr.RegistrationEndpoint,
RotationKey: dcr.RotationKey,
RegistrationProbe: dcr.RegistrationProbe,
SoftwareID: dcr.SoftwareID,
SoftwareStatement: dcr.SoftwareStatement,
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/thv-operator/pkg/controllerutil/authserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,7 @@ func TestBuildAuthServerRunConfig(t *testing.T) {
DCRConfig: &mcpv1beta1.DCRUpstreamConfig{
RegistrationEndpoint: "https://idp.example.com/register",
RotationKey: "2026-10",
RegistrationProbe: "authorizationEndpoint",
},
},
},
Expand All @@ -1832,6 +1833,7 @@ func TestBuildAuthServerRunConfig(t *testing.T) {
upstream.OAuth2Config.DCRConfig.RegistrationEndpoint)
assert.Empty(t, upstream.OAuth2Config.DCRConfig.DiscoveryURL)
assert.Equal(t, "2026-10", upstream.OAuth2Config.DCRConfig.RotationKey)
assert.Equal(t, "authorizationEndpoint", upstream.OAuth2Config.DCRConfig.RegistrationProbe)
// No InitialAccessTokenRef set: env var name should stay empty.
assert.Empty(t, upstream.OAuth2Config.DCRConfig.InitialAccessTokenEnvVar)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1736,21 +1736,38 @@ spec:
maxLength: 2048
pattern: ^https://[^\s?#]+[^/\s?#]$
type: string
registrationProbe:
description: |-
RegistrationProbe selects how ToolHive checks the cached upstream client
at start. tokenEndpoint (the default) redeems a random authorization
code with the cached client and reads invalid_client as "registration
gone"; it decides only for upstreams that authenticate the client
before validating the grant. authorizationEndpoint sends an
authorization request for the cached client without following
redirects and reads a direct 4xx as "gone" (RFC 6749 §4.1.2.1), for
upstreams whose token endpoint answers invalid_grant for any client_id;
do not use it behind bot protection that answers 4xx to non-browser
requests. none skips the check; the registration is then retired only
through RotationKey.
enum:
- tokenEndpoint
- authorizationEndpoint
- none
type: string
rotationKey:
description: |-
RotationKey is an optional operator-chosen label that is part of the
cached registration's identity but is never sent to the upstream.
Change it to make the proxies register a fresh client at their next
start instead of reusing the cached one: for example after the
upstream has dropped the registration, or when a rotation is required.
At start ToolHive checks the cached client at the upstream's token
endpoint and registers a new one by itself when the upstream answers
invalid_client. Where that check cannot tell (an upstream that answers
a dropped public client like a live one, or a private_key_jwt client),
the stale client_id is only rejected in the user's browser, and
changing this key is the remedy. The previous registration's row stays
in storage, and upstream refresh tokens bound to the previous client_id
stop refreshing, so affected users sign in again.
At start ToolHive checks the cached client at the upstream (see
RegistrationProbe) and registers a new one by itself when the upstream
rejects it. Where the configured probe cannot tell, the stale client_id
may only be rejected in the user's browser, and changing this key is
the remedy. The previous registration's row stays in storage, and
upstream refresh tokens bound to the previous client_id stop
refreshing, so affected users sign in again.
maxLength: 64
pattern: ^[A-Za-z0-9._-]+$
type: string
Expand Down Expand Up @@ -2144,21 +2161,38 @@ spec:
maxLength: 2048
pattern: ^https://[^\s?#]+[^/\s?#]$
type: string
registrationProbe:
description: |-
RegistrationProbe selects how ToolHive checks the cached upstream client
at start. tokenEndpoint (the default) redeems a random authorization
code with the cached client and reads invalid_client as "registration
gone"; it decides only for upstreams that authenticate the client
before validating the grant. authorizationEndpoint sends an
authorization request for the cached client without following
redirects and reads a direct 4xx as "gone" (RFC 6749 §4.1.2.1), for
upstreams whose token endpoint answers invalid_grant for any client_id;
do not use it behind bot protection that answers 4xx to non-browser
requests. none skips the check; the registration is then retired only
through RotationKey.
enum:
- tokenEndpoint
- authorizationEndpoint
- none
type: string
rotationKey:
description: |-
RotationKey is an optional operator-chosen label that is part of the
cached registration's identity but is never sent to the upstream.
Change it to make the proxies register a fresh client at their next
start instead of reusing the cached one: for example after the
upstream has dropped the registration, or when a rotation is required.
At start ToolHive checks the cached client at the upstream's token
endpoint and registers a new one by itself when the upstream answers
invalid_client. Where that check cannot tell (an upstream that answers
a dropped public client like a live one, or a private_key_jwt client),
the stale client_id is only rejected in the user's browser, and
changing this key is the remedy. The previous registration's row stays
in storage, and upstream refresh tokens bound to the previous client_id
stop refreshing, so affected users sign in again.
At start ToolHive checks the cached client at the upstream (see
RegistrationProbe) and registers a new one by itself when the upstream
rejects it. Where the configured probe cannot tell, the stale client_id
may only be rejected in the user's browser, and changing this key is
the remedy. The previous registration's row stays in storage, and
upstream refresh tokens bound to the previous client_id stop
refreshing, so affected users sign in again.
maxLength: 64
pattern: ^[A-Za-z0-9._-]+$
type: string
Expand Down Expand Up @@ -4678,21 +4712,38 @@ spec:
maxLength: 2048
pattern: ^https://[^\s?#]+[^/\s?#]$
type: string
registrationProbe:
description: |-
RegistrationProbe selects how ToolHive checks the cached upstream client
at start. tokenEndpoint (the default) redeems a random authorization
code with the cached client and reads invalid_client as "registration
gone"; it decides only for upstreams that authenticate the client
before validating the grant. authorizationEndpoint sends an
authorization request for the cached client without following
redirects and reads a direct 4xx as "gone" (RFC 6749 §4.1.2.1), for
upstreams whose token endpoint answers invalid_grant for any client_id;
do not use it behind bot protection that answers 4xx to non-browser
requests. none skips the check; the registration is then retired only
through RotationKey.
enum:
- tokenEndpoint
- authorizationEndpoint
- none
type: string
rotationKey:
description: |-
RotationKey is an optional operator-chosen label that is part of the
cached registration's identity but is never sent to the upstream.
Change it to make the proxies register a fresh client at their next
start instead of reusing the cached one: for example after the
upstream has dropped the registration, or when a rotation is required.
At start ToolHive checks the cached client at the upstream's token
endpoint and registers a new one by itself when the upstream answers
invalid_client. Where that check cannot tell (an upstream that answers
a dropped public client like a live one, or a private_key_jwt client),
the stale client_id is only rejected in the user's browser, and
changing this key is the remedy. The previous registration's row stays
in storage, and upstream refresh tokens bound to the previous client_id
stop refreshing, so affected users sign in again.
At start ToolHive checks the cached client at the upstream (see
RegistrationProbe) and registers a new one by itself when the upstream
rejects it. Where the configured probe cannot tell, the stale client_id
may only be rejected in the user's browser, and changing this key is
the remedy. The previous registration's row stays in storage, and
upstream refresh tokens bound to the previous client_id stop
refreshing, so affected users sign in again.
maxLength: 64
pattern: ^[A-Za-z0-9._-]+$
type: string
Expand Down Expand Up @@ -5086,21 +5137,38 @@ spec:
maxLength: 2048
pattern: ^https://[^\s?#]+[^/\s?#]$
type: string
registrationProbe:
description: |-
RegistrationProbe selects how ToolHive checks the cached upstream client
at start. tokenEndpoint (the default) redeems a random authorization
code with the cached client and reads invalid_client as "registration
gone"; it decides only for upstreams that authenticate the client
before validating the grant. authorizationEndpoint sends an
authorization request for the cached client without following
redirects and reads a direct 4xx as "gone" (RFC 6749 §4.1.2.1), for
upstreams whose token endpoint answers invalid_grant for any client_id;
do not use it behind bot protection that answers 4xx to non-browser
requests. none skips the check; the registration is then retired only
through RotationKey.
enum:
- tokenEndpoint
- authorizationEndpoint
- none
type: string
rotationKey:
description: |-
RotationKey is an optional operator-chosen label that is part of the
cached registration's identity but is never sent to the upstream.
Change it to make the proxies register a fresh client at their next
start instead of reusing the cached one: for example after the
upstream has dropped the registration, or when a rotation is required.
At start ToolHive checks the cached client at the upstream's token
endpoint and registers a new one by itself when the upstream answers
invalid_client. Where that check cannot tell (an upstream that answers
a dropped public client like a live one, or a private_key_jwt client),
the stale client_id is only rejected in the user's browser, and
changing this key is the remedy. The previous registration's row stays
in storage, and upstream refresh tokens bound to the previous client_id
stop refreshing, so affected users sign in again.
At start ToolHive checks the cached client at the upstream (see
RegistrationProbe) and registers a new one by itself when the upstream
rejects it. Where the configured probe cannot tell, the stale client_id
may only be rejected in the user's browser, and changing this key is
the remedy. The previous registration's row stays in storage, and
upstream refresh tokens bound to the previous client_id stop
refreshing, so affected users sign in again.
maxLength: 64
pattern: ^[A-Za-z0-9._-]+$
type: string
Expand Down
Loading
Loading