Use the pinned provider's id_token when its access token is opaque
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
Problem
resolveClaims guards against using the ToolHive-issued token's mirrored email/name when a provider is pinned. The test at pkg/authz/authorizers/cedar/core_test.go:1880 is named "the multi-upstream misattribution guard" and states the mirror "must NOT stand in, because in a chain it belongs to the first configured upstream, not this one."
That guard only applies on the path where the upstream access token parses as a JWT. On the opaque branch (pkg/authz/authorizers/cedar/core.go:707-716) the function returns requestClaims — the mirror — and never reads UpstreamIDTokens[provider], even though it is present for any OIDC upstream. supplementFromIDToken has a single call site, the parse-success path at core.go:721.
So we enforce the guard on one branch and violate it on the other.
Impact
Behavioral, not a security boundary. thv_claim_source already reports the degradation, docs/authz.md:357 documents it, and the pinned provider defaults to upstreams[0] — where the mirror and the id_token carry the same value from the same provider, so nothing is misattributed.
It matters in two narrower shapes:
- Multi-upstream chain with an explicit pin to a non-first upstream. Policies see the first upstream's
email/namerather than the pinned provider's. - Upstreams that assert profile claims only in the id_token. Profile-claim policies deny although the assertion is available — the #5916 deny-all shape, on the branch #6022 did not cover.
Proposed fix
Supplement email/name from the pinned provider's stored id_token on the opaque branch, keeping sub from the request token.
Do not simply call supplementFromIDToken(identity, jwt.MapClaims{}) and return that. profileClaimsFromIDToken is {name, email} — sub is deliberately excluded (core.go:801) — so the result would carry no sub, extractClientIDFromClaims (core.go:878-886) would fail, and every request on this path would return ErrMissingPrincipal. That turns a working path into deny-all, which is worse than the current behavior.
Sketch:
- Start from the request token's
subonly, so the Cedar principal entity ID is unchanged. - Supplement
email/namefromUpstreamIDTokens[provider]. - Give it a new claim-source label. The result is a hybrid — principal from the request token, profile claims from the pinned upstream — so it is neither
upstream:<provider>nor today'srequest:upstream-opaque. Reusingupstream:<provider>would also break theopaque_fallback_does_not_pass_as_upstreamtest, which exists to keep the degraded path from impersonating a healthy one. - No stored id_token for the provider: keep today's behavior and label.
- Leave the fail-closed path for JWT-shaped-but-unparsable tokens (
core.go:717-718) untouched.
Notes
- This newly permits. Any policy currently denying because profile claims were absent, or holding the mirror's value, starts matching. Needs a release note.
- Whether other request-token claims besides
subshould carry over is an open question — dropping them changes which policies match. Worth deciding explicitly rather than by accident. - Decide whether the new label should appear in the
docs/authz.md:349table as a fifth value, and whether the multi-upstream warning atdocs/authz.md:425-430moves out of the delegation section, since it applies here too.
Context
Reported via GHSA-ch9x-m6fv-82vp, closed as informative — the opaque fallback itself is intentional, added in #5147 (closing #5146) and disclosed through thv_claim_source, the docs, and a WARN log. The branch inconsistency above is the part worth fixing. Reporter: @Yanhaoxi.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in pkg/authz/authorizers/cedar/core.go:707-721 and compare the opaque branch with supplementFromIDToken and the tests in pkg/authz/authorizers/cedar/core_test.go around line 1880, including opaque_fallback_does_not_pass_as_upstream. Define the new hybrid claim-source label and decide how other request-token claims and documentation should be handled. Done means the pinned provider's id_token supplies profile claims without replacing the request token's sub, existing fail-closed behavior remains, and tests cover the fallback.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authorization
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100