Skip to content

Add RFC 7523 JWT Bearer grant package#5262

Merged
jhrozek merged 1 commit into
mainfrom
xaa-2-jwt-bearer-grant
May 12, 2026
Merged

Add RFC 7523 JWT Bearer grant package#5262
jhrozek merged 1 commit into
mainfrom
xaa-2-jwt-bearer-grant

Conversation

@jhrozek
Copy link
Copy Markdown
Contributor

@jhrozek jhrozek commented May 12, 2026

Summary

  • The XAA / ID-JAG flow (issue ToolHive should support the Identity-Assertion JWT Authorization Grant (ID-JAG / XAA) for cross-domain backend authentication #5218) is implemented in steps. "Step B" is the OAuth 2.0 JWT Bearer grant (RFC 7523 §2.1) used to exchange a signed JWT assertion at a target authorization server for an access token. This PR adds that primitive as a small, self-contained package so the XAA strategy and any other RFC 7523 caller can reuse it.
  • New package pkg/oauthproto/jwtbearer exposes a Config and TokenSource modeled on the sibling pkg/oauthproto/tokenexchange and built on the shared pkg/oauthproto helpers (NewFormRequest, DoTokenRequest, ParseTokenResponse, Redact).
  • TokenURL validation reuses pkg/networking.ValidateEndpointURL (RFC 6749 §3.2 — TLS required, localhost exception) and adds host, fragment, and embedded-userinfo checks to reject URLs that smuggle credentials.
  • HTTP errors are returned as *oauth2.RetrieveError with Body scrubbed before return, matching the stricter behavior of pkg/oauthproto/tokenexchange so error strings cannot leak raw upstream content into logs.
  • The package targets confidential clients per XAA / ID-JAG §8.1 (HTTP Basic auth, RFC 6749 §2.3.1); public-client client_id-in-body is intentionally not supported.
  • Also adds the GrantTypeJWTBearer URN constant to pkg/oauthproto.

Refs #5218

Type of change

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

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

go test ./pkg/oauthproto/jwtbearer/ covers: TokenURL validation (scheme, host, fragment, userinfo, required fields), String() redaction of secrets and assertion, success path with and without scopes, and the jwtbearer-specific error paths (assertion-provider error, RetrieveError unwrap + Body scrub, empty token_type rejection per RFC 6749 §5.1).

Does this introduce a user-facing change?

No. This is a library-internal primitive; no CLI surface, CRD, or runtime behavior changes yet. The first user-facing consumer (the XAA / ID-JAG strategy) will land in a follow-up PR.

Implementation plan

Approved implementation plan

Package scope:

  • Implements RFC 7523 §2.1 only (JWT-as-grant). Client authentication is independent: HTTP Basic via oauthproto.NewFormRequest per RFC 6749 §2.3.1. No JWT client auth (RFC 7523 §2.2), no client_secret_post, no body-only client_id — these are out of scope for the XAA consumer and YAGNI for now.
  • Mirrors pkg/oauthproto/tokenexchange: Config with Validate() / String() / TokenSource(ctx), internal tokenSource with Token(). buildFormData is private and only emits the three RFC 7523 §2.1 fields (grant_type, assertion, optional scope).

Security posture (informed by parallel multi-agent review):

  • Reject TokenURL with embedded userinfo to prevent credential leakage to the AS / via logs.
  • Scrub RetrieveError.Body before return so logged errors cannot include raw upstream content (matches tokenexchange).
  • Redact() used in String() for ClientSecret and the assertion-presence marker.
  • TLS enforced via networking.ValidateEndpointURL (localhost dev exception).

Validation tightening on top of oauthproto.ParseTokenResponse:

  • Empty token_type is rejected at the jwtbearer layer (RFC 6749 §5.1) since the shared helper is intentionally permissive.

Test scope discipline:

  • Tests cover only jwtbearer-specific behavior; paths that exercise oauthproto.ParseTokenResponse or NewFormRequest branches alone are out of scope and intentionally not tested here (those have their own tests).

Special notes for reviewers

  • pkg/oauthproto/tokenexchange/exchange.go is the closest sibling and a useful reference for parallel structure; one minor follow-up worth doing later is normalizing the error-message prefix in both packages (jwtbearer: / tokenexchange:).
  • The package is currently unused — no consumers in this PR by design. The XAA strategy lands separately so this PR stays small and reviewable.

🤖 Generated with Claude Code

Add pkg/oauthproto/jwtbearer, a self-contained client for the JWT
Bearer token grant defined by RFC 7523 Section 2.1. The grant exchanges
a signed JWT assertion for an OAuth 2.0 access token at a target
authorization server's token endpoint.

TokenURL validation reuses pkg/networking.ValidateEndpointURL plus
host, fragment, and userinfo checks to enforce RFC 6749 Section 3.2
(token endpoints must use TLS) and to reject URLs that smuggle
credentials in the URL itself. The token_type field in the success
response is validated as required by RFC 6749 Section 5.1. HTTP error
responses are returned as *oauth2.RetrieveError (from
golang.org/x/oauth2) with the raw Body scrubbed before return, matching
pkg/oauthproto/tokenexchange's stricter behavior so error strings
cannot leak upstream content into logs.

Client authentication is HTTP Basic per RFC 6749 Section 2.3.1; the
package targets confidential clients per XAA / ID-JAG §8.1 and does not
support public-client identification via a body client_id parameter.

Also add the GrantTypeJWTBearer URN constant to pkg/oauthproto, the
first consumer of which is this package.

The jwtbearer package is the Step B primitive used by the XAA (ID-JAG)
strategy. It has no XAA-specific behaviour at this layer and works for
any RFC 7523 JWT-Bearer exchange with a confidential client.
@jhrozek jhrozek requested a review from JAORMX as a code owner May 12, 2026 11:45
@github-actions github-actions Bot added the size/M Medium PR: 300-599 lines changed label May 12, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Codecov Report

❌ Patch coverage is 88.88889% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.03%. Comparing base (030594a) to head (118fd16).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
pkg/oauthproto/jwtbearer/grant.go 88.88% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5262      +/-   ##
==========================================
+ Coverage   68.02%   68.03%   +0.01%     
==========================================
  Files         616      617       +1     
  Lines       63005    63098      +93     
==========================================
+ Hits        42857    42927      +70     
- Misses      16945    16962      +17     
- Partials     3203     3209       +6     

☔ 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.

@jhrozek jhrozek merged commit 110b9a7 into main May 12, 2026
44 checks passed
@jhrozek jhrozek deleted the xaa-2-jwt-bearer-grant branch May 12, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants