Repository navigation
Let DCR upstreams choose the token endpoint auth method - #6986
Merged
Merged
Conversation
An OAuth2 upstream with dcrConfig always registered with the token endpoint auth method the resolver prefers: client_secret_basic when advertised, and by default on the registrationEndpoint branch, where no metadata is read. An upstream whose token endpoint does not accept that method, such as one that advertises client_secret_basic but reads client credentials only from the request body, failed every code exchange and refresh, and tokenEndpointAuthMethod could not help: with dcrConfig it failed validation for want of a secret source, and the negotiated method replaced it anyway. With dcrConfig, tokenEndpointAuthMethod now selects the method the client registers with. The resolver refuses a configured method the upstream does not advertise, and a registration response that names another one. A cached registration made with another method is replaced at the next resolve through the same compare-and-set as a rejected one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aron <aron@muonspace.com>
aron-muon
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
aponcedeleonch,
blkt,
jerm-dro,
jhrozek,
rdimitrov,
reyortiz3 and
tgrunnagle
as code owners
October 29, 2026 00:35
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6986 +/- ##
==========================================
+ Coverage 80.29% 80.35% +0.06%
==========================================
Files 847 849 +2
Lines 86142 86679 +537
==========================================
+ Hits 69165 69654 +489
- Misses 16972 17020 +48
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
jhrozek
reviewed
Oct 30, 2026
A DCR upstream with a configured client_secret_post or client_secret_basic method failed at start: the resolved run config dropped DCRConfig but kept the method, and validating that copy demanded a secret file. The resolution carries the method to the built config, so the resolved copy no longer keeps it. The resolver checked the method only on its own registration response. A cache write can return a row a replica on an older configuration stored first, and a concurrent caller shares the leader's result, so the final resolution is now refused when its method differs from the configured one. The next start or recheck replaces the row. The registrationProbe none docs now say it turns off the upstream probe, not the method check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aron <aron@muonspace.com>
1 task
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An OAuth2 upstream with
dcrConfigalways registers with the token endpoint auth method the resolver prefers. That isclient_secret_basicwhen the upstream advertises it, and also by default on theregistrationEndpointbranch, where no metadata is read. An upstream whose token endpoint does not accept that method fails every code exchange and refresh, and the operator has no way out:oauth2Config.tokenEndpointAuthMethodfails validation next todcrConfig, and the negotiated method would replace it anyway. We hit this with an upstream that advertisesclient_secret_basicbut reads client credentials only from the request body (invalid_client: Missing client_id).dcrConfig,oauth2Config.tokenEndpointAuthMethodnow selects the method the client registers with (dcr.Request.TokenEndpointAuthMethod). Run-config validation no longer asks for a client secret source in that case, since the secret comes from the registration. The resolved run config hands the method to the built provider config through the resolution, together with the registered secret.nonewithout S256 PKCE, and a confidential method on a public client. It also refuses a registration response that names a method other than the configured one, and a final resolution that still names one. That happens when a cache write returns a row a replica on an older configuration stored first, or when a concurrent caller configured differently shares the leader's singleflight result.ReplaceIfUnchangedpath a rejected registration takes, so changing the field takes effect at the next start without a rotation key.Fixes #6985
Type of change
Test plan
task test)task test-e2e)task lint-fix)New tests:
TestResolveDCRCredentials_ConfiguredAuthMethod(selection and every refusal),TestResolveCredentials_CacheHitConfiguredAuthMethod(replace on mismatch without probing, keep on match, error from a store withoutReplacer),TestResolveDCRCredentials_AuthoritativeWinnerWithOtherAuthMethodErrors(a stored winner with another method),TestBuildUpstreamConfigs_DCRConfiguredAuthMethod(both confidential methods through registration and a cache hit, asserting the built config's method and registered secret),TestConsumeResolution_ClearsDCRFields, plus cases inTestNewDCRRequestandTestOAuth2UpstreamRunConfigValidate.task testpasses except three packages outside this change on this machine:pkg/skills/identitytokenfails the same way on main,pkg/container/dockerneeds real Squid and Envoy containers, andpkg/lockfileis timing-sensitive under full-suite load.API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.The field already exists; only its documented meaning with
dcrConfigchanges.Changes
pkg/auth/dcr/request.goRequest.TokenEndpointAuthMethodpkg/auth/dcr/resolver.gopkg/authserver/runner/dcr_adapter.gopkg/authserver/config.godcr_config; field doccmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.godocs/arch/11-auth-server-storage.mddocs/operator/crd-api.md, swagger, SDKDoes this introduce a user-facing change?
Yes, for OAuth2 upstreams with
dcrConfig:tokenEndpointAuthMethodfailed at start withrequires client_secret_file or client_secret_env_var, and the client always registered with the preferred method.registrationProbesays. A configured method the upstream does not advertise, or a registration the upstream switched to another method, fails at start with an error naming both methods. A stored row with another method that a replica on an older configuration wrote during a rollout fails that start or recheck with anauth_method_mismatcherror, and the next one replaces the row.Upstreams that leave the field unset behave as before.
Implementation plan
Approved implementation plan
OAuth2UpstreamConfig.TokenEndpointAuthMethodrather than add adcrConfigfield, since it already means "how this client authenticates at the token endpoint".dcr.Requestand letselectTokenEndpointAuthMethodreturn it ahead of the preference order, checked against the advertised set and the S256 gate.ReplaceIfUnchanged, instead of folding the method into the cache key.dcr_configis set, and clear the method on the resolved run config so the resolution supplies it.Special notes for reviewers
rotationKey. A row registered with the configured method keeps being used, and only a mismatching row is replaced, so turning the field on does not orphan a working registration.registrationProbe: nonestill runs the method check.Replacerreturns an error on a mismatch, where the probe path hands back the cached row unchecked. Using that row would present credentials with the method the operator configured away from.tokenEndpointAuthMethodfield, so this covers OAuth2 upstreams only; Basic auth for OIDC upstreams is Support Basic auth for OIDC upstreams #6552.🤖 Generated with Claude Code