Skip to content

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

Merged
tgrunnagle merged 3 commits into
stacklok:mainfrom
juzerpatanwala:fix-rfc8414-path-aware-issuer-discovery
Jun 1, 2026
Merged

dcr: support RFC 8414 §3.1 path-insertion in discovery-URL → issuer derivation#5395
tgrunnagle merged 3 commits into
stacklok:mainfrom
juzerpatanwala:fix-rfc8414-path-aware-issuer-discovery

Conversation

@juzerpatanwala
Copy link
Copy Markdown
Contributor

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
…ack)

Per @tgrunnagle's review on stacklok#5395: the path-insertion arms introduced in
the previous commit accidentally regress one edge case. For an input
where the path ends `/.well-known/oauth-authorization-server/` (trailing
slash, no tenant), the suffix arms don't match (suffix test sees the
trailing "/"), so the HasPrefix arm fires and TrimPrefix leaves
`u.Path = "/"` → spurious issuer `https://host/` that fails the §3.3
byte-equality check against the upstream's declared `https://host`.

Before this PR the same input hit the `default` arm and produced
`https://host` correctly.

Fix: after TrimPrefix in each path-insertion arm, collapse a lone
`/` back to empty so the trailing-slash form converges on the same
origin issuer the bare-suffix and `default` arms produce.

Tightens the inline comment to describe both shapes that reach the
HasPrefix arm (real tenant suffix and trailing-slash). Adds two
table-driven cases — one each for the oauth and oidc trailing-slash
forms — to lock in the expected origin output.

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.

Re-reviewed after 6d1d7cc. The trailing-slash regression (finding A) is cleanly resolved — u.Path == "/" is collapsed back to "" in both path-insertion arms, the misleading comment is corrected to describe both shapes that reach the branch, and two new table cases (oauth + oidc trailing-slash → origin) pin the behavior. No new issues introduced; the normalization fires only on a bare /, so real tenant paths are untouched. Core fix is spec-faithful and fails safe against the §3.3 check.

Approving. The operator-configured DiscoveryURL e2e regression test (finding B) is a non-blocking suggestion rather than a merge blocker — the added unit cases now cover the path-insertion and trailing-slash derivations directly, so the helper's §3.3-relevant output is well pinned. An end-to-end test driving the full DiscoveryURL discovery flow would still be a nice follow-up to guard the derive→§3.3 round-trip, but it's optional given the unit coverage.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the size/XS Extra small PR: < 100 lines changed label Jun 1, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.78%. Comparing base (5a8692d) to head (fb9dac4).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5395      +/-   ##
==========================================
+ Coverage   68.76%   68.78%   +0.02%     
==========================================
  Files         629      629              
  Lines       63922    63930       +8     
==========================================
+ Hits        43958    43977      +19     
+ Misses      16707    16698       -9     
+ Partials     3257     3255       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tgrunnagle tgrunnagle merged commit 05ca226 into stacklok:main Jun 1, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XS Extra small PR: < 100 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants