Skip to content

DCR rejects RFC 8414 §3.1 path-insertion discoveryUrl with issuer-mismatch error #5390

Description

@tgrunnagle

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 defaultu.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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    authbugSomething isn't workinggoPull requests that update go codeoauth

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions