Skip to content

Add JWT-bearer assertion grant - #6391

Merged
jhrozek merged 1 commit into
mainfrom
6336-6337-jwt-bearer-grant
Aug 20, 2026
Merged

jhrozek merged 1 commit into
mainfrom
6336-6337-jwt-bearer-grant

Conversation

@jhrozek

@jhrozek jhrozek commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • ToolHive needs a constrained way for trusted external workloads to obtain MCP access tokens without registering a ToolHive OAuth client; this adds the plain RFC 7523 JWT-bearer assertion grant and keeps its trust boundary separate from RFC 8693 delegation.
  • Validate trusted issuer signatures, authorization-server audiences, assertion age, exact subject/resource bindings, replay consumption, asymmetric signing algorithms, and ID-token markers before issuing an access token.
  • Expose the per-issuer policy through MCPExternalAuthConfig, publish it in discovery and generated API schemas only when enabled, and share issuer validation/JWKS state with token exchange.
  • Add replay-safe memory and Redis storage, synthetic client persistence for clientless requests, generated documentation, and unit/integration/operator E2E coverage.

Fixes #6336
Fixes #6337

Type of change

  • New feature

Test plan

  • Unit tests (task test)
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)
  • Manual testing (describe below)

task lint passed. task test-e2e was attempted but is blocked before feature setup by the locally installed ToolHive Desktop CLI conflict guard; it fails across the suite, independent of this branch.

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.

Changes

File area Change
pkg/authserver/server/tokenexchange/ Add the RFC 7523 handler, validation, replay checks, shared-validator wiring, and regression coverage.
pkg/authserver/storage/ Persist single-use assertion consumption safely across memory, Redis, and CIMD-decorated storage.
cmd/thv-operator/ and CRD artifacts Configure issuer JWT-bearer policies, including accepted AS audiences, through the operator API.
docs/ Regenerate server/operator API references and document the trust, replay, and storage model.
test/e2e/ Exercise token issuance and MCP proxy access through the operator.

Does this introduce a user-facing change?

Yes. Administrators can configure trusted external issuers to exchange signed RFC 7523 assertions for short-lived, resource-bound ToolHive access tokens without OAuth client credentials.

Implementation plan

Approved implementation plan
  1. Add trusted-issuer policy and replay-safe storage for a plain RFC 7523 JWT-bearer grant.
  2. Validate assertions and issue bounded, resource-scoped tokens through the embedded authorization server.
  3. Expose the policy through the operator CRD, generate API artifacts, and verify the real operator-to-proxy flow.
  4. Harden the resulting feature with focused security, validation, persistence, and documentation review fixes.

Special notes for reviewers

  • The handler intentionally overwrites any resolved request client with a synthetic public client. This preserves clientless-request persistence and prevents supplied DCR clients from affecting registration-TTL renewal.
  • acceptedAudiences identifies authorization-server identities, not resources. Startup rejects an overlap with allowedAudiences so a resource-scoped upstream access token cannot be used as a clientless assertion.

@github-actions github-actions Bot added the size/XL Extra large PR: 1000+ lines changed label Aug 19, 2026
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.93737% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.70%. Comparing base (39a85c8) to head (28d33a4).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...hserver/server/tokenexchange/jwt_bearer_handler.go 85.85% 29 Missing ⚠️
...ver/server/tokenexchange/multi_issuer_validator.go 78.44% 25 Missing ⚠️
...perator/api/v1beta1/mcpexternalauthconfig_types.go 30.76% 9 Missing ⚠️
pkg/authserver/storage/memory.go 80.00% 5 Missing ⚠️
pkg/authserver/config.go 75.00% 3 Missing ⚠️
pkg/authserver/storage/redis.go 86.95% 3 Missing ⚠️
pkg/authserver/server/tokenexchange/factory.go 81.81% 2 Missing ⚠️
pkg/authserver/server_impl.go 90.00% 2 Missing ⚠️
pkg/authserver/runner/embeddedauthserver.go 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6391      +/-   ##
==========================================
- Coverage   77.72%   77.70%   -0.02%     
==========================================
  Files         748      750       +2     
  Lines       71826    72563     +737     
==========================================
+ Hits        55827    56387     +560     
- Misses      15994    16171     +177     
  Partials        5        5              

☔ View full report in Codecov by Harness.
📢 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.

rdimitrov
rdimitrov previously approved these changes Aug 19, 2026
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from 4b606e6 to d34682b Compare August 19, 2026 19:11
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 19, 2026
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from d34682b to 15a3f77 Compare August 19, 2026 19:34
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 19, 2026
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from 15a3f77 to 78939f2 Compare August 19, 2026 19:55
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 19, 2026
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from 8670611 to 97a15ef Compare August 19, 2026 21:06
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 19, 2026
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from 97a15ef to 6c3d528 Compare August 19, 2026 21:28
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 19, 2026
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from 6c3d528 to c02553c Compare August 19, 2026 21:48
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 19, 2026
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from c02553c to 13bcb80 Compare August 20, 2026 06:49
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 20, 2026
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from 13bcb80 to a472668 Compare August 20, 2026 08:57
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 20, 2026

@samuv samuv left a comment

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.

Thanks for the thorough implementation and the unusually clear trust-model documentation. I found six issues worth addressing before merge. The most immediate one is that the operator validation converter drops JWTBearerGrant, which causes grant-only configurations to be rejected. I also left focused comments on supplied Basic credentials, OAuth error classification, replay-store failures, Redis synthetic-client identification, and mixed RFC 7523/RFC 8693 configuration validation.

Checklist

  • Tests: CI unit, integration, and E2E checks are currently passing. A reconciliation/status assertion is still needed for the grant-only operator path.
  • Docs: Updated and detailed.
  • Registry impact: None.
  • Security: Client authentication and replay-store error handling need attention.
  • Backwards compatibility: The v1beta1 additions are additive, but mixed-mode validation should reject unusable configurations at admission time.

Comment thread pkg/authserver/server/tokenexchange/jwt_bearer_handler.go Outdated
Comment thread pkg/authserver/storage/redis.go
Comment thread pkg/authserver/server/tokenexchange/jwt_bearer_handler.go Outdated
Comment thread pkg/authserver/server/tokenexchange/jwt_bearer_handler.go Outdated
Comment thread cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go
Comment thread cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from a472668 to a28562f Compare August 20, 2026 10:28
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 20, 2026
@jhrozek

jhrozek commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — all six findings confirmed and fixed:

  • Basic-auth credential bypass in CanSkipClientAuth
  • replay-store failures now return a server error instead of invalid_grant
  • validateAssertionType now returns invalid_grant per RFC 7523
  • buildTrustedIssuerConfigs was dropping JWTBearerGrant (the grant-only-issuer bug) — also added the reconcile/Valid=True status test you asked for
  • tightened the mixed RFC 7523/8693 validation at both the CEL and Go level
  • synthetic client IDs are now rejected at registration time so a colliding admin-configured client can't be reconstructed as unregistered after a Redis round-trip

Replied inline on each thread with the specific fix. CI's green again.

Allow trusted external issuers to exchange signed RFC 7523 assertions directly for bounded ToolHive access tokens, without a registered OAuth client.

Validate the issuer, signature, authorization-server audience, assertion age, and exact subject/resource binding. Consume each assertion before issuance to prevent replay, including assertions without a jti claim. Use a synthetic client only to keep the clientless request persistence-safe.

Expose issuer policy through MCPExternalAuthConfig, share trusted-issuer validation with RFC 8693 token exchange, and advertise the grant only when configured.

Fixes #6336
Fixes #6337
@jhrozek
jhrozek force-pushed the 6336-6337-jwt-bearer-grant branch from a28562f to 28d33a4 Compare August 20, 2026 11:48
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Aug 20, 2026

@samuv samuv left a comment

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.

Thanks for the follow-up. I rechecked the two remaining items on 28d33a4:

  • CanSkipClientAuth now keys off BasicAuth() ok alone, including an empty-username header. The test helper no longer injects a password into credential-free cases, and there is an explicit empty-username regression.
  • Mixed RFC 7523/RFC 8693 validation now treats non-empty allowedActors as a delegation signal in both CEL and validateDelegationPolicy, with admission and runtime coverage.

The earlier findings (grant conversion, replay-store errors, invalid_grant classification, and the reserved synthetic prefix) still look solid. CI is green. LGTM.

@jhrozek
jhrozek merged commit 8735438 into main Aug 20, 2026
53 checks passed
@jhrozek
jhrozek deleted the 6336-6337-jwt-bearer-grant branch August 20, 2026 12:15
@github-actions github-actions Bot mentioned this pull request Aug 26, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR: 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Declare trust in external assertion issuers and map claims to authority Accept third-party JWT assertions as an authorization grant

3 participants