Skip to content

Validate aud and resource claims in returned ID-JAG JWT#5716

Open
jhrozek wants to merge 1 commit into
mainfrom
xaa-validate-aud-resource
Open

Validate aud and resource claims in returned ID-JAG JWT#5716
jhrozek wants to merge 1 commit into
mainfrom
xaa-validate-aud-resource

Conversation

@jhrozek

@jhrozek jhrozek commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • The XAA IdP exchange (RFC 8693) returned an ID-JAG JWT without validating its aud or resource claims against the values requested in the exchange. If the IdP minted an ID-JAG for a different audience or resource, ToolHive forwarded it to the target AS unchanged — a token-misbinding / confused-deputy risk.
  • Added validateIDJAGClaims to check the aud claim (REQUIRED per ID-JAG draft §3.1, membership check per RFC 7519 §4.1.3) and the resource claim (OPTIONAL per §3.1, validated when one was requested) inside the returned ID-JAG JWT. A non-JWT assertion that fails to parse is now rejected rather than forwarded.
  • The typ header check remains logged-only by design (the draft §3.1 MUST is on the issuer; ToolHive is the holder/presenter).

Fixes #5696

Type of change

  • Bug fix

Test plan

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

Does this introduce a user-facing change?

No. The XAA strategy now rejects malformed ID-JAG responses that would have failed at the target AS anyway. No configuration or API surface change.

Implementation plan

Approved implementation plan

Validate the aud and resource claims in the ID-JAG JWT returned by the IdP exchange in the XAA auth strategy (pkg/vmcp/auth/strategies/xaa.go).

Per draft-ietf-oauth-identity-assertion-authz-grant §3.1:

  • aud is REQUIRED — the issuer identifier of the Resource Authorization Server.
  • resource is OPTIONAL — "either a single URI or an array of URIs" (RFC 7519 string-or-array semantics).

Step 1 — validateIDJAGClaims + wiring:

  • Add validateIDJAGClaims(claims jwt.MapClaims, wantAudience, wantResource string) error.
  • aud: membership check (requested audience must be among the aud values; IdP may include additional audiences). Handle string and []interface{} array forms (RFC 7519 §4.1.3).
  • resource: membership check only when wantResource != "" (OPTIONAL in the exchange request per §4.3). Handle string and array forms.
  • Fatal on mismatch/missing. JWT parse failure is also fatal (a non-JWT assertion cannot be a valid ID-JAG).
  • typ header check stays logged-only (draft §3.1 MUST is on the issuer).

Step 2 — Tests:

  • Generalize buildJWTWithTypHeader into buildIDJAGJWT(t, typ, aud, resource).
  • Fix existing fixtures that returned non-JWT strings or JWTs without aud.
  • Add cases: aud missing/wrong-string/wrong-array/string-match/array-with-extra; resource wrong/missing-when-requested/array-match/not-configured; non-JWT parse failure.

Reviewed by MoE panel: oauth-expert (ID-JAG spec compliance), code-reviewer (Go conventions), security-advisor (threat model). All approved.

Special notes for reviewers

  • The resource claim is validated only when the operator configured a TargetResource. Per the ID-JAG draft §3.1, resource is OPTIONAL in both the exchange request (§4.3) and the ID-JAG itself. When configured, the check is stricter than the draft requires (rejecting a missing resource even though the IdP could legitimately omit it) — this is an intentional operator-driven binding choice for defence-in-depth.
  • iss/sub/exp/jti are not validated locally. This is acceptable deferral — the target AS performs authoritative JWT validation in the target grant (RFC 7523). An exp check would be cheap defence-in-depth and is flagged as follow-up.
  • The oauth-expert reviewer noted the RFC citation for the resource array form: RFC 8707 §2 defines the request parameter, not the JWT claim. The comments cite "RFC 7519 string-or-array semantics" instead.

Generated with Claude Code

The XAA IdP exchange (RFC 8693) returned an ID-JAG JWT without
validating its aud or resource claims against the values requested in
the exchange. If the IdP minted an ID-JAG for a different audience or
resource, ToolHive forwarded it to the target AS unchanged — a token-
misbinding / confused-deputy risk.

Add validateIDJAGClaims to check the aud claim (REQUIRED per draft
§3.1, membership check per RFC 7519 §4.1.3) and the resource claim
(OPTIONAL per §3.1, validated when one was requested) inside the
returned ID-JAG JWT. A non-JWT assertion that fails to parse is now
rejected rather than forwarded. The typ header check remains logged-
only by design (the draft §3.1 MUST is on the issuer; ToolHive is the
holder/presenter).

Closes #5696
@github-actions github-actions Bot added the size/M Medium PR: 300-599 lines changed label Jul 3, 2026
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.66%. Comparing base (7d08e2e) to head (cdab20f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/vmcp/auth/strategies/xaa.go 95.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5716   +/-   ##
=======================================
  Coverage   70.66%   70.66%           
=======================================
  Files         682      682           
  Lines       68854    68887   +33     
=======================================
+ Hits        48656    48682   +26     
- Misses      16650    16656    +6     
- Partials     3548     3549    +1     

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

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.

xaa strategy: validate returned ID-JAG aud/resource claims before Step B (fail-fast config-hygiene check)

2 participants