Allow Cedar delegation without an upstream session - #6506
Conversation
RFC 8693 delegated tokens and RFC 7523 JWT-bearer tokens link to no upstream
IdP login, so a configured primary provider rejected them before Cedar ever
evaluated policy.
Both grants can be recognised from the token itself, so let them through on
what the token states rather than on what is missing from the identity. An
absent Identity.UpstreamTokens map says only that no credential was loaded,
never why: an anonymous identity, a local identity and a bearer token from an
unpinned IdP look exactly like a delegated one, and admitting all of them
would let claims from a trust root the deployment never pinned drive policy.
Fall back to request-token claims only when the token affirmatively declares
it has no upstream session. Delegation already says so through the act claim,
parsed into Identity.DelegationChain. The JWT-bearer grant has no standard
marker, so the authorization server stamps a namespaced private claim on the
tokens it mints for that grant. A nil map with neither signal keeps the deny,
as does a session that exists but lacks the pinned provider's credentials.
Two supporting changes keep that boundary visible. The vMCP incoming-auth
factory now rejects a pinned primaryUpstreamProvider under local or anonymous
auth, which can never produce an upstream session; the operator enforces the
same rule, but it does not run for a hand-written vmcp config. And
Identity.UpstreamTokens documents that a nil map states no reason, so no
future reader repeats the inference.
Policies still could not tell which trust root asserted the claims they read,
including on the pre-existing path where an opaque upstream access token
silently degrades to request claims. Every evaluation now carries a
thv_claim_source attribute on the principal and the context naming that root.
It is written after claim prefixing and outside the claim_ namespace, so a
token cannot spoof its own provenance. Purely additive: no existing policy
reads it.
Two things to know when deploying this:
- JWT-bearer tokens minted by an authorization server running the previous
version carry no marker, so a pinned deployment denies them until those
tokens turn over. Delegated tokens are unaffected, since the act claim
they already carry needs no new plumbing.
- The DEBUG log in resolveClaims now reports the claim source using the same
vocabulary as the new attribute, rather than a second private one. The
"source" field changes from token, no-session-fallback, token-fallback and
upstream to request, request:no-upstream-session, request:upstream-opaque
and upstream:<provider>.
Fixes #6424
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6506 +/- ##
=======================================
Coverage 78.25% 78.26%
=======================================
Files 770 770
Lines 75436 75458 +22
=======================================
+ Hits 59035 59057 +22
Misses 16396 16396
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
JAORMX
left a comment
There was a problem hiding this comment.
Re-review at c70f529b80616d76de4094d02812ad3e09442727:
-
Security / authorization boundary bypass —
pkg/authz/authorizers/cedar/core.go:611-624,699-708permits request-claim fallback whenever a token carries a well-formedactclaim orhttps://toolhive.dev/no_upstream_session: true. Neither signal is bound to authenticated evidence that the token was minted by this deployment’s embedded authorization server.pkg/vmcp/auth/factory/incoming.go:88-101only requires OIDC, so an externally issued but valid incoming OIDC token can assert either claim, have no upstream credential, and bypass the configuredprimaryUpstreamProviderclaim-source boundary. The collision-resistant private-claim name prevents accidents, not deliberate assertions.Establish issuance/grant provenance at the authentication boundary after issuer validation and store it as typed identity state; Cedar should consume that state rather than wire claims. At a minimum, restrict this fallback to the configured embedded auth-server issuer and reject a pinned provider where the embedded-auth trust boundary is absent. Please add negative coverage for a foreign OIDC issuer whose valid tokens carry
actand the marker, as well as an end-to-end positive RFC 8693 path through real incoming-auth validation and Cedar evaluation.
task test passes locally in the PR worktree, but current tests hand-construct identities and do not cover this cross-boundary spoofing case.
Summary
actchain and a private JWT-bearer marker minted by ToolHive's authorization server.Fixes #6424
Type of change
Test plan
task test)task test-e2e)task lint-fix)API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Changes
pkg/authz/authorizers/cedarpkg/auth*pkg/vmcp/auth/factorydocs/authz.mdDoes this introduce a user-facing change?
Yes. Cedar policies with
primaryUpstreamProvidercan authorize RFC 8693 delegated and RFC 7523 JWT-bearer tokens using their request-token claims when the token explicitly declares it has no upstream session. Other identities without the configured provider's credential remain denied.Special notes for reviewers
The fallback is deliberately narrower than treating every nil
UpstreamTokensmap as session-less. A nil map can also represent local, anonymous, or un-enriched identities, so it is insufficient proof that request claims should be trusted under a pinned provider. The newthv_claim_sourceattribute lets policies opt into requiring claims from the configured upstream provider.