Skip to content

dcr: support RFC 8414 §3.1 path-insertion in discovery-URL → issuer derivation#5395

Open
juzerpatanwala wants to merge 1 commit into
stacklok:mainfrom
juzerpatanwala:fix-rfc8414-path-aware-issuer-discovery
Open

dcr: support RFC 8414 §3.1 path-insertion in discovery-URL → issuer derivation#5395
juzerpatanwala wants to merge 1 commit into
stacklok:mainfrom
juzerpatanwala:fix-rfc8414-path-aware-issuer-discovery

Conversation

@juzerpatanwala
Copy link
Copy Markdown

Summary

deriveExpectedIssuerFromDiscoveryURL (in pkg/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 in oauthproto.FetchAuthorizationServerMetadataFromURL can be applied.

It already handled:

  • Suffix-append form for single-tenant providers — e.g. https://mcp.atlassian.com/.well-known/oauth-authorization-serverhttps://mcp.atlassian.com
  • Suffix-append with issuer-as-prefix for some multi-tenant providers — e.g. https://idp.example.com/tenants/acme/.well-known/openid-configurationhttps://idp.example.com/tenants/acme

The 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_endpoint as 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/mcp declares issuer: https://mcp.us5.datadoghq.com/v1/mcp, and DCR fails with:

issuer mismatch (RFC 8414 §3.3):
  expected "https://mcp.us5.datadoghq.com",
  got      "https://mcp.us5.datadoghq.com/v1/mcp"

This PR closes that gap.

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

How

Add two new branches to the switch in deriveExpectedIssuerFromDiscoveryURL that recognise the well-known segment as a path prefix (HasPrefix(path, suffix+"/")) and trim just that segment to recover origin + tenant path:

case strings.HasPrefix(u.Path, oauthSuffix+"/"):
    u.Path = strings.TrimPrefix(u.Path, oauthSuffix)
case strings.HasPrefix(u.Path, oidcSuffix+"/"):
    u.Path = strings.TrimPrefix(u.Path, oidcSuffix)

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

  • Unit tests — go test ./pkg/auth/dcr/... ./pkg/oauthproto/... (all packages pass)
  • E2E tests (task test-e2e) — not run locally; covered by repo CI
  • Linting (task lint-fix) — gofmt -l clean; go vet clean; golangci-lint not available locally, relying on CI

New unit cases (added to the existing table-driven TestDeriveExpectedIssuerFromDiscoveryURL):

Discovery URL Expected issuer
https://mcp.us5.datadoghq.com/.well-known/oauth-authorization-server/v1/mcp https://mcp.us5.datadoghq.com/v1/mcp
https://idp.example.com/.well-known/oauth-authorization-server/tenants/acme https://idp.example.com/tenants/acme
https://idp.example.com/.well-known/openid-configuration/tenants/acme https://idp.example.com/tenants/acme

All eight pre-existing cases still pass.

API Compatibility

  • This PR does not break the v1beta1 API, OR the api-break-allowed label 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

File Change
pkg/auth/dcr/resolver.go deriveExpectedIssuerFromDiscoveryURL recognises the path-insertion form; opt-out comment replaced with a third recognised convention
pkg/auth/dcr/resolver_test.go 3 new table-driven cases for path-insertion (OAuth, multi-segment tenant, OIDC)

Does this introduce a user-facing change?

Yes — operators with a dcr_config.discovery_url pointing at an RFC 8414 §3.1 path-insertion discovery document (e.g. Datadog's MCP at mcp.us5.datadoghq.com) now succeed instead of failing the issuer-equality check. No values changes are required.

…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-serverhttps://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>
Copy link
Copy Markdown
Contributor

@tgrunnagle tgrunnagle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread pkg/auth/dcr/resolver.go
// 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+"/"):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 defaulthttps://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:

Suggested change
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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants