DCR discovery rejects RFC 8414 §3.1 path-insertion discoveryUrl with issuer-mismatch error
Summary
When dcrConfig.discoveryUrl points at an RFC 8414 §3.1 path-insertion URL — where the well-known suffix is inserted between host and the issuer's path (e.g. https://example.com/.well-known/oauth-authorization-server/v1/mcp for an issuer https://example.com/v1/mcp) — DCR fails with:
issuer mismatch (RFC 8414 §3.3): expected "https://mcp.us5.datadoghq.com", got "https://mcp.us5.datadoghq.com/v1/mcp"
ToolHive recovers the expected issuer by stripping /.well-known/... from the URL only when that suffix is at the end of the path. For the path-insertion form, the suffix is in the middle of the path, so the strip falls into the origin-only fallback and the §3.3 equality check rejects the AS's correctly-formed metadata. Currently ToolHive only accepts the legacy "well-known at root" and the OIDC issuer-suffix conventions.
Datadog's MCP server (mcp.us5.datadoghq.com) is one such path-aware authorization server, so any user pointing ToolHive at a Datadog-hosted MCP server via discoveryUrl is blocked.
Per the spec
RFC 8414 §3.1 says the well-known URI is formed by inserting the well-known path component between host and the issuer's path component, e.g.:
- Issuer:
https://example.com/v1/mcp
- Well-known URL:
https://example.com/.well-known/oauth-authorization-server/v1/mcp
§3.3 then requires the metadata's issuer field to equal the original issuer (https://example.com/v1/mcp), component-wise — host and path. ToolHive currently compares against host only for this URL shape.
Reproduction
dcr_config.yaml:
dcr_config:
discovery_url: https://mcp.us5.datadoghq.com/.well-known/oauth-authorization-server/v1/mcp
Expected: discovery succeeds; DCR proceeds; the workload enters running state.
Actual:
issuer mismatch (RFC 8414 §3.3): expected "https://mcp.us5.datadoghq.com", got "https://mcp.us5.datadoghq.com/v1/mcp"
Root cause
deriveExpectedIssuerFromDiscoveryURL in pkg/auth/dcr/resolver.go:920-946 handles only two URL shapes:
switch {
case strings.HasSuffix(u.Path, oauthSuffix): // "/.well-known/oauth-authorization-server" at end
u.Path = strings.TrimSuffix(u.Path, oauthSuffix)
case strings.HasSuffix(u.Path, oidcSuffix): // "/.well-known/openid-configuration" at end
u.Path = strings.TrimSuffix(u.Path, oidcSuffix)
default:
// Custom (non-well-known) discovery URL — fall back to origin.
u.Path = ""
}
For https://mcp.us5.datadoghq.com/.well-known/oauth-authorization-server/v1/mcp:
- Path =
/.well-known/oauth-authorization-server/v1/mcp
- Neither
HasSuffix matches — the suffix is followed by /v1/mcp.
- Falls into
default → u.Path = "" → returns origin https://mcp.us5.datadoghq.com.
The AS returns metadata with issuer: "https://mcp.us5.datadoghq.com/v1/mcp". The §3.3 check in pkg/oauthproto/discovery.go:314-316 then rejects it.
The gap is explicitly acknowledged in the function's doc comment at pkg/auth/dcr/resolver.go:916-919:
RFC 8414 §3.1's path-aware form (well-known path inserted between host and tenant path, e.g. https://example.com/.well-known/oauth-authorization-server/tenant) is not auto-detected here — operators on that pattern can switch to dcr_config.registration_endpoint to bypass discovery.
This issue tracks closing that gap so the path-insertion form works without operators having to fall back to registration_endpoint.
Suggested fix
In deriveExpectedIssuerFromDiscoveryURL (pkg/auth/dcr/resolver.go), recognize the path-insertion form by checking whether /.well-known/oauth-authorization-server or /.well-known/openid-configuration appears inside the path (not only as a suffix). When the inner-suffix form matches, the trailing segment after the well-known path is the issuer's path component, e.g.:
/.well-known/oauth-authorization-server/v1/mcp → issuer path = /v1/mcp
/.well-known/openid-configuration/tenant/acme → issuer path = /tenant/acme
Care needed:
- Resolve ambiguity with the existing issuer-suffix form (
/tenants/acme/.well-known/oauth-authorization-server): prefer the suffix-at-end interpretation when the path ends with the well-known component, fall through to the path-insertion interpretation only when there is content after the well-known component.
- The well-known segment must appear with a leading
/ and be a full path component (not a substring) to avoid false positives.
Test coverage
Add cases to TestDeriveExpectedIssuerFromDiscoveryURL in pkg/auth/dcr/resolver_test.go:
{
name: "oauth path-insertion (RFC 8414 §3.1) with tenant path suffix",
discoveryURL: "https://mcp.us5.datadoghq.com/.well-known/oauth-authorization-server/v1/mcp",
want: "https://mcp.us5.datadoghq.com/v1/mcp",
},
{
name: "oidc path-insertion with tenant path suffix",
discoveryURL: "https://idp.example.com/.well-known/openid-configuration/tenant/acme",
want: "https://idp.example.com/tenant/acme",
},
Also add an end-to-end regression test in pkg/auth/discovery/dcr_resolver_test.go mirroring the style of TestHandleDynamicRegistration_NonRootIssuerRFC8414PathInsertion (added in #5357 for the auto-discovery branch) but exercising the operator-configured DiscoveryURL branch.
Workaround
Until this is fixed, operators targeting an AS that only exposes the path-insertion form can bypass discovery by setting dcr_config.registration_endpoint directly (and supplying authorization_endpoint, token_endpoint, and scopes explicitly).
Related
DCR discovery rejects RFC 8414 §3.1 path-insertion
discoveryUrlwith issuer-mismatch errorSummary
When
dcrConfig.discoveryUrlpoints at an RFC 8414 §3.1 path-insertion URL — where the well-known suffix is inserted between host and the issuer's path (e.g.https://example.com/.well-known/oauth-authorization-server/v1/mcpfor an issuerhttps://example.com/v1/mcp) — DCR fails with:ToolHive recovers the expected issuer by stripping
/.well-known/...from the URL only when that suffix is at the end of the path. For the path-insertion form, the suffix is in the middle of the path, so the strip falls into the origin-only fallback and the §3.3 equality check rejects the AS's correctly-formed metadata. Currently ToolHive only accepts the legacy "well-known at root" and the OIDC issuer-suffix conventions.Datadog's MCP server (
mcp.us5.datadoghq.com) is one such path-aware authorization server, so any user pointing ToolHive at a Datadog-hosted MCP server viadiscoveryUrlis blocked.Per the spec
RFC 8414 §3.1 says the well-known URI is formed by inserting the well-known path component between host and the issuer's path component, e.g.:
https://example.com/v1/mcphttps://example.com/.well-known/oauth-authorization-server/v1/mcp§3.3 then requires the metadata's
issuerfield to equal the original issuer (https://example.com/v1/mcp), component-wise — host and path. ToolHive currently compares against host only for this URL shape.Reproduction
dcr_config.yaml:Expected: discovery succeeds; DCR proceeds; the workload enters running state.
Actual:
Root cause
deriveExpectedIssuerFromDiscoveryURLinpkg/auth/dcr/resolver.go:920-946handles only two URL shapes:For
https://mcp.us5.datadoghq.com/.well-known/oauth-authorization-server/v1/mcp:/.well-known/oauth-authorization-server/v1/mcpHasSuffixmatches — the suffix is followed by/v1/mcp.default→u.Path = ""→ returns originhttps://mcp.us5.datadoghq.com.The AS returns metadata with
issuer: "https://mcp.us5.datadoghq.com/v1/mcp". The §3.3 check inpkg/oauthproto/discovery.go:314-316then rejects it.The gap is explicitly acknowledged in the function's doc comment at
pkg/auth/dcr/resolver.go:916-919:This issue tracks closing that gap so the path-insertion form works without operators having to fall back to
registration_endpoint.Suggested fix
In
deriveExpectedIssuerFromDiscoveryURL(pkg/auth/dcr/resolver.go), recognize the path-insertion form by checking whether/.well-known/oauth-authorization-serveror/.well-known/openid-configurationappears inside the path (not only as a suffix). When the inner-suffix form matches, the trailing segment after the well-known path is the issuer's path component, e.g.:/.well-known/oauth-authorization-server/v1/mcp→ issuer path =/v1/mcp/.well-known/openid-configuration/tenant/acme→ issuer path =/tenant/acmeCare needed:
/tenants/acme/.well-known/oauth-authorization-server): prefer the suffix-at-end interpretation when the path ends with the well-known component, fall through to the path-insertion interpretation only when there is content after the well-known component./and be a full path component (not a substring) to avoid false positives.Test coverage
Add cases to
TestDeriveExpectedIssuerFromDiscoveryURLinpkg/auth/dcr/resolver_test.go:{ name: "oauth path-insertion (RFC 8414 §3.1) with tenant path suffix", discoveryURL: "https://mcp.us5.datadoghq.com/.well-known/oauth-authorization-server/v1/mcp", want: "https://mcp.us5.datadoghq.com/v1/mcp", }, { name: "oidc path-insertion with tenant path suffix", discoveryURL: "https://idp.example.com/.well-known/openid-configuration/tenant/acme", want: "https://idp.example.com/tenant/acme", },Also add an end-to-end regression test in
pkg/auth/discovery/dcr_resolver_test.gomirroring the style ofTestHandleDynamicRegistration_NonRootIssuerRFC8414PathInsertion(added in #5357 for the auto-discovery branch) but exercising the operator-configuredDiscoveryURLbranch.Workaround
Until this is fixed, operators targeting an AS that only exposes the path-insertion form can bypass discovery by setting
dcr_config.registration_endpointdirectly (and supplyingauthorization_endpoint,token_endpoint, andscopesexplicitly).Related
discoveryUrlconfigured). The operator-configureddiscoveryUrlbranch was not addressed and is what this issue covers.