Conversation
A trusted issuer configured for the JWT-bearer grant previously accepted both plain RFC 7523 assertions and ID-JAG cross-app-access assertions. That made trusting an issuer for service-account assertions also trust it as an IdP for a different resource owner without an explicit decision. Add AcceptedAssertionTypes (jwt_bearer|id_jag) to JWTBearerGrantConfig and use it to filter each handler's issuer policies independently. The enum list preserves the three intended choices: plain assertions only, ID-JAG only, or both. Nil and empty runtime values default to jwt_bearer so standalone and hand-built configurations preserve their existing behavior; invalid runtime values are rejected. ID-JAG policies must list this authorization server's issuer in AcceptedAudiences, matching the ID-JAG audience requirement and failing invalid configurations at startup. When both assertion types are selected, they intentionally share subject bindings, so the CRD and runtime documentation make clear that a binding also authorizes credential-free plain JWT-bearer redemption. Co-authored-by: mecatl <noreply@mecatl.dev>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6678 +/- ##
==========================================
+ Coverage 79.11% 79.12% +0.01%
==========================================
Files 786 786
Lines 78538 78576 +38
==========================================
+ Hits 62138 62176 +38
Misses 16395 16395
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
aron-muon
left a comment
There was a problem hiding this comment.
Ran this branch through the same calibrated environment used to verify #6677 (whole-repo task targets, with a known baseline of 12 packages that fail identically on pristine main for environment reasons — workloads tmp-path tests, a pkg/api/v1 panic, network-dependent retriever, etc.):
task test: only the 12 baseline packages fail; zero new failures. Alltokenexchangetests pass, including the new filtering/gating coverage. (The socket-path failure your test plan mentions didn't reproduce here, consistent with it being a tmpdir-length environment issue.)task lint: two real findings that will fail the Lint job —lll(>130 chars) on both newAcceptedAssertionTypesfield lines:pkg/authserver/inbound_grants.go:43(137 chars)pkg/authserver/server/tokenexchange/multi_issuer_validator.go:160(134 chars)
On the design: agreed that accepting ID-JAGs implicitly wherever a jwt-bearer policy exists widened the issuer trust decision without an explicit opt-in — #6677 consciously piggybacked on the existing policy to avoid new config surface, and this is the right correction. The fail-loud config validation (accepted_audiences must include the AS issuer when id_jag is enabled) also matches what we hit in practice: our live Okta deployment already carries the bare issuer URL in acceptedAudiences because Okta mints the JAG aud from the resource connection's audience field, so real-world configs pass this check naturally.
Two observations, neither blocking:
-
Upgrade note worth a line in the release notes: a deployment that enabled ID-JAG under #6677's behavior (ours is presumably the only one) silently loses it on upgrade until
accepted_assertion_types: [jwt_bearer, id_jag]is added. For deployments running dev images on released charts, the new CRD field is pruned by the apiserver until the next chart release ships updated CRDs — so the practical migration there is "hold the image pin until the chart lands, or hand-apply the updated CRDs." Fine for us, just worth stating so nobody debugs a silentinvalid_grantregression. -
Diagnostics regression at the no-opt-in boundary: when no issuer accepts
id_jag, the bound handler isn't registered at all, so a recognizedoauth-id-jag+jwtassertion falls through to fosite's genericinvalid_request("...redirect_uri..." boilerplate) — the exact hard-to-diagnose failure mode that motivated #6676 (open question 3 there). The constructor's at-least-one-issuer requirement is why registration is gated, so one option is relaxing that requirement for the bound handler specifically: registered-with-empty-policies would reject with its precise "issuer is not enabled for this grant" hint instead. Happy to leave that as a follow-up either way.
The shared-subject-bindings consequence being documented rather than split per-type seems right for now — splitting bindings per assertion type would double the config surface for a case nobody has yet.
Two lll violations on the new AcceptedAssertionTypes struct-tag lines cannot be wrapped, so mark them nolint like other unwrappable tag/marker lines in this repo. Register IDJAGIssuanceFactory whenever the jwt-bearer grant type is enabled, even with zero opted-in issuers, so a well-formed "oauth-id-jag+jwt" assertion is claimed and rejected with the precise "issuer is not enabled for this grant" hint instead of falling through every registered handler to fosite's generic invalid_request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
acceptedAssertionTypesselection for plainjwt_bearer,id_jag, or both; default omitted values to plain JWT-bearer for backwards compatibility.Type of change
Test plan
task test) — attempted; the suite reaches tests but an unrelated Unix-socket test fails because the harness temporary path exceeds the platform socket-path limit (pkg/server/discovery/health_test.go:174).task test-e2e)task lint-fix)task operator-manifests,task crdref-gen, andgit diff --check; regenerated CRDs and API reference documentation successfully.API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Changes
cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.gocmd/thv-operator/api/v1beta1/zz_generated.deepcopy.gocmd/thv-operator/pkg/controllerutil/authserver.gocmd/thv-operator/pkg/controllerutil/authserver_inbound_grants_test.gocmd/thv-operator/pkg/controllerutil/authserver_test.godeploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yamldeploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yamldeploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yamldeploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yamldocs/arch/17-token-exchange-delegation.mddocs/operator/crd-api.mddocs/server/docs.godocs/server/swagger.jsondocs/server/swagger.yamlpkg/authserver/inbound_grants.gopkg/authserver/inbound_grants_test.gopkg/authserver/server/tokenexchange/id_jag_handler.gopkg/authserver/server/tokenexchange/id_jag_handler_test.gopkg/authserver/server/tokenexchange/jwt_bearer_handler.gopkg/authserver/server/tokenexchange/multi_issuer_validator.gopkg/authserver/server/tokenexchange/multi_issuer_validator_test.gopkg/authserver/server_impl.gopkg/authserver/server_test.gosdk/go/client/oas_json_gen.gosdk/go/client/oas_schemas_gen.gosdk/go/client/oas_validators_gen.gosdk/go/openapi.jsonsdk/go/openapi.yamlDoes this introduce a user-facing change?
Yes. Operators can select which assertion forms each trusted issuer accepts. Existing omitted configuration continues to accept only plain RFC 7523 JWT-bearer assertions; accepting ID-JAG now requires explicit
id_jagselection and an audience entry for this authorization server's issuer.Special notes for reviewers
When an issuer accepts both assertion types, every configured subject/resource binding deliberately authorizes both forms. In particular, a subject added for ID-JAG is also eligible for credential-free plain JWT-bearer redemption; configure both only when that authority is intended.
Generated with Claude Code