Follow-up from review of RFC-0079 (stacklok/toolhive-rfcs#79) and merged PR #5684.
Context: XAAStrategy.performStepA (pkg/vmcp/auth/strategies/xaa.go) already unverified-parses the returned ID-JAG JWT to check the typ header (lines 260-269), as defense-in-depth alongside the OAuth-level issued_token_type/token_type=N_A checks (lines 233-245). It does not currently also check the JWT's aud/resource claims against the configured TargetAudience/TargetResource before handing the assertion to Step B.
Why this is worth adding: it is not a security boundary — an attacker controlling a malicious/compromised IdP response could fabricate matching unsigned claims just as easily, so this does not substitute for the Resource AS's own signature/aud/resource validation at Step B, which remains the actual trust boundary (per RFC-0079 §4.3). Its value is as a fail-fast config-correctness check: catching IdP misconfiguration or an accidental cross-wiring between resources before an opaque Step B failure (or, in a lax-AS scenario, a silent success against the wrong resource).
Scope: in performStepA, extend the existing parser.ParseUnverified(assertion, jwt.MapClaims{}) call (already at line 260-261) to also read aud and resource from the parsed claims and compare against config.targetAudience/config.targetResource. Treat a mismatch as a hard error (unlike the existing typ check, which is a soft warning) — this one should block Step B rather than just log, since proceeding to Step B with a known-wrong audience serves no purpose. Log clearly that this is a config-hygiene check, not a signature-verified assertion.
Related: #5695 (XAA follow-ups from #5691) tracks other deferred non-blocking XAA items but doesn't cover this one.
cc @tgrunnagle — this was your finding (F2) on RFC-0079's review.
Follow-up from review of RFC-0079 (stacklok/toolhive-rfcs#79) and merged PR #5684.
Context:
XAAStrategy.performStepA(pkg/vmcp/auth/strategies/xaa.go) already unverified-parses the returned ID-JAG JWT to check thetypheader (lines 260-269), as defense-in-depth alongside the OAuth-levelissued_token_type/token_type=N_Achecks (lines 233-245). It does not currently also check the JWT'saud/resourceclaims against the configuredTargetAudience/TargetResourcebefore handing the assertion to Step B.Why this is worth adding: it is not a security boundary — an attacker controlling a malicious/compromised IdP response could fabricate matching unsigned claims just as easily, so this does not substitute for the Resource AS's own signature/
aud/resourcevalidation at Step B, which remains the actual trust boundary (per RFC-0079 §4.3). Its value is as a fail-fast config-correctness check: catching IdP misconfiguration or an accidental cross-wiring between resources before an opaque Step B failure (or, in a lax-AS scenario, a silent success against the wrong resource).Scope: in
performStepA, extend the existingparser.ParseUnverified(assertion, jwt.MapClaims{})call (already at line 260-261) to also readaudandresourcefrom the parsed claims and compare againstconfig.targetAudience/config.targetResource. Treat a mismatch as a hard error (unlike the existingtypcheck, which is a soft warning) — this one should block Step B rather than just log, since proceeding to Step B with a known-wrong audience serves no purpose. Log clearly that this is a config-hygiene check, not a signature-verified assertion.Related: #5695 (XAA follow-ups from #5691) tracks other deferred non-blocking XAA items but doesn't cover this one.
cc @tgrunnagle — this was your finding (F2) on RFC-0079's review.