Skip to content

Allow Cedar delegation without an upstream session - #6506

Open
jhrozek wants to merge 1 commit into
mainfrom
6424-cedar-upstream-tokens
Open

Allow Cedar delegation without an upstream session#6506
jhrozek wants to merge 1 commit into
mainfrom
6424-cedar-upstream-tokens

Conversation

@jhrozek

@jhrozek jhrozek commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • RFC 8693 delegated tokens and RFC 7523 JWT-bearer tokens have no upstream login, so a pinned Cedar upstream provider rejected them before policy evaluation. This restores evaluation for those deliberately session-less tokens while preserving fail-closed behavior for identities that merely lack upstream credentials.
  • Recognize the two intentional no-session grants from verified token signals: delegation's act chain and a private JWT-bearer marker minted by ToolHive's authorization server.
  • Preserve the pinned-provider boundary for missing or wrong upstream credentials, reject incompatible local/anonymous vMCP configurations, expose non-spoofable claim provenance to Cedar policies, and document the resulting behavior.

Fixes #6424

Type of change

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

Test plan

  • Unit tests (task test)
  • E2E tests (task test-e2e)
  • Linting (task lint-fix)
  • Manual testing (reviewed the branch diff and verification cases for delegated tokens, JWT-bearer tokens, absent/mismatched upstream credentials, opaque upstream tokens, and local/anonymous incoming-auth configuration.)

API Compatibility

  • This PR does not break the v1beta1 API, OR the api-break-allowed label is applied and the migration guidance is described above.

Changes

File area Change
pkg/authz/authorizers/cedar Select claims safely by upstream-session provenance and expose claim source.
pkg/auth* Parse delegation state and stamp/consume the JWT-bearer no-session marker.
pkg/vmcp/auth/factory Reject pinned-provider configurations that cannot have an upstream session.
docs/authz.md Document claim provenance and session-less grant behavior.

Does this introduce a user-facing change?

Yes. Cedar policies with primaryUpstreamProvider can 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 UpstreamTokens map 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 new thv_claim_source attribute lets policies opt into requiring claims from the configured upstream provider.

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
@github-actions github-actions Bot added the size/L Large PR: 600-999 lines changed label Sep 3, 2026
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.26%. Comparing base (7f0d08f) to head (c70f529).

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

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at c70f529b80616d76de4094d02812ad3e09442727:

  • Security / authorization boundary bypasspkg/authz/authorizers/cedar/core.go:611-624,699-708 permits request-claim fallback whenever a token carries a well-formed act claim or https://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-101 only requires OIDC, so an externally issued but valid incoming OIDC token can assert either claim, have no upstream credential, and bypass the configured primaryUpstreamProvider claim-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 act and 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large PR: 600-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC 8693 delegated tokens are denied all tools when Cedar authz uses an upstream provider as its primary claim source

2 participants