dcr: support RFC 8414 §3.1 path-insertion in discovery-URL → issuer derivation#5395
Conversation
…erivation `deriveExpectedIssuerFromDiscoveryURL` recovers the issuer the upstream is expected to claim in its discovery document. It already handled the suffix-append form (e.g. https://mcp.atlassian.com/.well-known/oauth-authorization-server → https://mcp.atlassian.com) and the issuer-suffix multi-tenant style (.../tenants/acme/.well-known/openid-configuration → .../tenants/acme), but the comment block explicitly opted out of the RFC 8414 §3.1 path-insertion form — operators on that pattern had to fall back to `dcr_config.registration_endpoint` to bypass discovery entirely. That gap rejects providers that publish a path-component issuer per the letter of the RFC. Datadog's MCP authorization server is one such provider: its discovery URL `https://mcp.us5.datadoghq.com/.well-known/oauth-authorization-server/v1/mcp` declares issuer `https://mcp.us5.datadoghq.com/v1/mcp`, and DCR discovery aborts with: issuer mismatch (RFC 8414 §3.3): expected "https://mcp.us5.datadoghq.com", got "https://mcp.us5.datadoghq.com/v1/mcp" Recognise the path-insertion form by checking for the well-known segment as a path *prefix* followed by a tenant path (HasPrefix(path, suffix+"/")), trimming just the well-known segment to recover origin + tenant path. Disambiguated from the existing suffix-append case by position: the well-known segment at the end of the path is suffix-append; at the start with more path following is path-insertion. The two cases cannot both match a single URL. Tests cover the new branch for both the OAuth and OIDC suffix variants plus a multi-segment tenant. All existing cases continue to pass. Per RFC 8414 §3 (the well-known URI is formed by inserting the well-known suffix between host and path of the issuer) and RFC 8615 (well-known URI conventions). Signed-off-by: Juzer Patanwala <juzer.patanwala@project44.com>
tgrunnagle
left a comment
There was a problem hiding this comment.
Multi-agent review summary
Tight, well-scoped bug fix that closes the RFC 8414 §3.1 path-insertion gap in deriveExpectedIssuerFromDiscoveryURL for the operator-configured discoveryUrl branch. The approach matches the issue's suggested fix exactly — two HasPrefix(path, suffix+"/") arms ordered after the suffix arms, with the trailing-/ guard disambiguating suffix-append from path-insertion. The well-known segment is matched as a full path component (leading / in the constant + trailing / guard), so substrings can't false-match, and any mis-derivation fails safe against the exact-match §3.3 check. Core defect resolved; 0 HIGH-severity findings.
| # | Finding | Severity | Consensus |
|---|---|---|---|
| A | Trailing-slash bare well-known now derives issuer with spurious / (regression + false comment) |
LOW | 8/10 |
| B | Missing operator-configured DiscoveryURL e2e regression test the issue required |
MEDIUM | 9/10 |
B — Missing e2e regression test (MEDIUM)
Issue #5390 lists two test deliverables: unit cases on the helper (✅ added) and an e2e regression test in pkg/auth/discovery/dcr_resolver_test.go mirroring TestHandleDynamicRegistration_NonRootIssuerRFC8414PathInsertion (#5357) but driving the operator-configured DiscoveryURL branch. Only the unit cases landed. The helper is verified in isolation, but nothing proves the path-bearing issuer survives the full discovery flow and clears the §3.3 check in the DiscoveryURL branch — which is exactly where the original error surfaced.
Suggestion: add an httptest-based test serving metadata at /.well-known/oauth-authorization-server/v1/mcp with issuer = server.URL+"/v1/mcp", drive the DiscoveryURL branch, and assert DCR succeeds end-to-end. If you consciously decided the unit cases suffice, a note in "Special notes for reviewers" would capture that, since it deviates from the linked issue.
3 specialist agents (RFC 8414 correctness, Go quality, test coverage). Codex cross-review skipped — CLI not installed.
🤖 Generated with Claude Code
| // suffix (e.g. /.well-known/oauth-authorization-server) is already covered | ||
| // by the suffix cases above; we only enter this branch when there is a | ||
| // tenant path after the well-known segment. | ||
| case strings.HasPrefix(u.Path, oauthSuffix+"/"): |
There was a problem hiding this comment.
[CORRECTNESS · LOW] Trailing-slash bare well-known derives a spurious trailing-slash issuer (minor regression).
For https://host/.well-known/oauth-authorization-server/ the suffix arms don't match (path ends in /), so this HasPrefix arm fires: TrimPrefix leaves u.Path = "/" → issuer https://host/. Before this PR the same input hit default → https://host. The comment above ("a bare suffix … is already covered by the suffix cases above") is therefore inaccurate for the trailing-slash form, and the component-sensitive §3.3 exact-string check would reject the https://host/ derivation.
Narrow (discovery URLs rarely carry a trailing slash), but worth tightening:
| case strings.HasPrefix(u.Path, oauthSuffix+"/"): | |
| case strings.HasPrefix(u.Path, oauthSuffix+"/"): | |
| u.Path = strings.TrimPrefix(u.Path, oauthSuffix) | |
| if u.Path == "/" { | |
| u.Path = "" | |
| } |
Also consider correcting the comment and adding a table case for the trailing-slash shape expecting https://host.
Summary
deriveExpectedIssuerFromDiscoveryURL(inpkg/auth/dcr/resolver.go) recovers the issuer the upstream is expected to claim in its discovery document, so the RFC 8414 §3.3 equality check inoauthproto.FetchAuthorizationServerMetadataFromURLcan be applied.It already handled:
https://mcp.atlassian.com/.well-known/oauth-authorization-server→https://mcp.atlassian.comhttps://idp.example.com/tenants/acme/.well-known/openid-configuration→https://idp.example.com/tenants/acmeThe function explicitly opted out of the RFC 8414 §3.1 / RFC 8615 path-insertion form — where the well-known segment is inserted between the host and the issuer's tenant path. The existing comment block punted to
dcr_config.registration_endpointas the workaround.That gap rejects providers that publish a path-component issuer per the letter of the RFC. Datadog's MCP authorization server is one such provider: its discovery URL
https://mcp.us5.datadoghq.com/.well-known/oauth-authorization-server/v1/mcpdeclaresissuer: https://mcp.us5.datadoghq.com/v1/mcp, and DCR fails with:This PR closes that gap.
Type of change
How
Add two new branches to the
switchinderiveExpectedIssuerFromDiscoveryURLthat recognise the well-known segment as a path prefix (HasPrefix(path, suffix+"/")) and trim just that segment to recover origin + tenant path:Disambiguation from the existing suffix-append case is positional: well-known at the end of the path is suffix-append; at the start with more path following is path-insertion. The two cases cannot both match a single URL — guarded by the trailing
/so a bare suffix (already handled above) doesn't take this branch.The opt-out comment in the function docstring is replaced with a third "recognised convention" entry describing the new behaviour.
Test plan
go test ./pkg/auth/dcr/... ./pkg/oauthproto/...(all packages pass)task test-e2e) — not run locally; covered by repo CItask lint-fix) —gofmt -lclean;go vetclean;golangci-lintnot available locally, relying on CINew unit cases (added to the existing table-driven
TestDeriveExpectedIssuerFromDiscoveryURL):https://mcp.us5.datadoghq.com/.well-known/oauth-authorization-server/v1/mcphttps://mcp.us5.datadoghq.com/v1/mcphttps://idp.example.com/.well-known/oauth-authorization-server/tenants/acmehttps://idp.example.com/tenants/acmehttps://idp.example.com/.well-known/openid-configuration/tenants/acmehttps://idp.example.com/tenants/acmeAll eight pre-existing cases still pass.
API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Pure function-internal change. Pre-existing values entries (issuer-suffix and bare-suffix forms) continue to derive the same issuer they did before.
Changes
pkg/auth/dcr/resolver.goderiveExpectedIssuerFromDiscoveryURLrecognises the path-insertion form; opt-out comment replaced with a third recognised conventionpkg/auth/dcr/resolver_test.goDoes this introduce a user-facing change?
Yes — operators with a
dcr_config.discovery_urlpointing at an RFC 8414 §3.1 path-insertion discovery document (e.g. Datadog's MCP atmcp.us5.datadoghq.com) now succeed instead of failing the issuer-equality check. No values changes are required.