Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/server/docs.go

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

4 changes: 2 additions & 2 deletions docs/server/swagger.json

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

27 changes: 23 additions & 4 deletions docs/server/swagger.yaml

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

54 changes: 50 additions & 4 deletions pkg/authserver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,14 +266,44 @@ type OAuth2UpstreamRunConfig struct {
// points at RFC 8414 / OIDC Discovery metadata from which the registration
// endpoint is resolved; RegistrationEndpoint is used directly when the upstream
// does not publish discovery metadata.
//
// Trust assumption: DiscoveryURL and RegistrationEndpoint are operator-supplied
// URLs validated only for HTTPS-or-loopback. The DCR resolver will issue
// outbound HTTP requests — possibly carrying the RFC 7591 initial access token
// as a bearer header — to whatever address those URLs resolve to. There is
// currently no allowlist or RFC1918 / link-local / cloud-metadata-service
// guard, because the operator role is fully trusted today. If the trust
// boundary ever changes (e.g. a multi-tenant operator deployment, or a less-
// privileged role gains write access to this struct via a CRD or YAML
// surface), this field becomes a confused-deputy SSRF vector. Hardening is
// tracked in https://github.com/stacklok/toolhive/issues/5135.
type DCRUpstreamConfig struct {
// DiscoveryURL is the RFC 8414 / OIDC Discovery URL from which the
// registration_endpoint is resolved at runtime. Mutually exclusive with
// RegistrationEndpoint.
// DiscoveryURL is the exact RFC 8414 / OIDC Discovery document URL to
// fetch at runtime. The resolver issues a single GET against this URL
// (no well-known-path fallback) and reads registration_endpoint,
// authorization_endpoint, token_endpoint,
// token_endpoint_auth_methods_supported, and scopes_supported from the
// response. Per RFC 8414 §3.3, the document's "issuer" field must
// exactly match the upstream issuer configured on the parent
// run-config.
//
// Use this field when the upstream publishes discovery metadata at a
// path that differs from the issuer-derived well-known paths — for
// example a multi-tenant IdP whose metadata lives at
// https://idp.example.com/tenants/acme/.well-known/openid-configuration.
//
// Mutually exclusive with RegistrationEndpoint.
DiscoveryURL string `json:"discovery_url,omitempty" yaml:"discovery_url,omitempty"`

// RegistrationEndpoint is the RFC 7591 registration endpoint URL used
// directly, bypassing discovery. Mutually exclusive with DiscoveryURL.
// directly, bypassing discovery. Because no discovery is performed,
// server-capability fields (token_endpoint_auth_methods_supported,
// scopes_supported) are unavailable on this code path; the caller is
// expected to also supply AuthorizationEndpoint, TokenEndpoint, and an
// explicit Scopes list on the parent OAuth2UpstreamRunConfig. Auth
// method falls back to the resolver's default (client_secret_basic).
//
// Mutually exclusive with DiscoveryURL.
RegistrationEndpoint string `json:"registration_endpoint,omitempty" yaml:"registration_endpoint,omitempty"`
Comment thread
tgrunnagle marked this conversation as resolved.

// InitialAccessTokenFile is the path to a file containing the RFC 7591
Expand Down Expand Up @@ -507,6 +537,22 @@ func (c *OAuth2UpstreamRunConfig) Validate() error {
if err := c.DCRConfig.Validate(); err != nil {
return fmt.Errorf("oauth2 upstream: invalid dcr_config: %w", err)
}

// When the operator configures DCRConfig.RegistrationEndpoint, the
// resolver bypasses discovery and therefore cannot populate
// AuthorizationEndpoint or TokenEndpoint from server metadata. The
// run-config must supply both explicitly or the upstream is
// unusable: registration would succeed and the first authorize or
// token-exchange call would silently fail with empty endpoints.
// Discovery flow (DCRConfig.DiscoveryURL) is unaffected — those
// fields populate from metadata.
if c.DCRConfig.RegistrationEndpoint != "" {
if c.AuthorizationEndpoint == "" || c.TokenEndpoint == "" {
return fmt.Errorf(
"oauth2 upstream: authorization_endpoint and token_endpoint are required " +
"when dcr_config.registration_endpoint is set (no discovery to populate them)")
}
}
}

return nil
Expand Down
38 changes: 37 additions & 1 deletion pkg/authserver/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,49 @@ func TestOAuth2UpstreamRunConfigValidate(t *testing.T) {
errMsg: "either discovery_url or registration_endpoint is required",
},
{
name: "DCRConfig with only registration_endpoint is valid",
name: "DCRConfig with only registration_endpoint is valid when authorization_endpoint and token_endpoint are also set",
config: OAuth2UpstreamRunConfig{
AuthorizationEndpoint: "https://idp.example.com/authorize",
TokenEndpoint: "https://idp.example.com/token",
DCRConfig: &DCRUpstreamConfig{
RegistrationEndpoint: "https://idp.example.com/register",
},
},
},

// registration_endpoint requires explicit authorize/token endpoints.
// Discovery would have populated them; bypassing discovery means the
// run-config must supply them or the upstream is unusable.
{
name: "DCRConfig.registration_endpoint without authorization_endpoint rejects",
config: OAuth2UpstreamRunConfig{
TokenEndpoint: "https://idp.example.com/token",
DCRConfig: &DCRUpstreamConfig{
RegistrationEndpoint: "https://idp.example.com/register",
},
},
wantErr: true,
errMsg: "authorization_endpoint and token_endpoint are required",
},
{
name: "DCRConfig.registration_endpoint without token_endpoint rejects",
config: OAuth2UpstreamRunConfig{
AuthorizationEndpoint: "https://idp.example.com/authorize",
DCRConfig: &DCRUpstreamConfig{
RegistrationEndpoint: "https://idp.example.com/register",
},
},
wantErr: true,
errMsg: "authorization_endpoint and token_endpoint are required",
},
{
name: "DCRConfig.discovery_url is valid without explicit endpoints (discovery populates them)",
config: OAuth2UpstreamRunConfig{
DCRConfig: &DCRUpstreamConfig{
DiscoveryURL: "https://idp.example.com/.well-known/oauth-authorization-server",
},
},
},
}

for _, tt := range tests {
Expand Down
Loading
Loading