Skip to content

Fall back to request-token claims for opaque upstream tokens#5147

Merged
jhrozek merged 1 commit into
stacklok:mainfrom
cjohnhanson:fix/cedar-opaque-token-fallback
Jun 1, 2026
Merged

Fall back to request-token claims for opaque upstream tokens#5147
jhrozek merged 1 commit into
stacklok:mainfrom
cjohnhanson:fix/cedar-opaque-token-fallback

Conversation

@cjohnhanson

Copy link
Copy Markdown

Summary

  • VirtualMCPServer's incoming Cedar authorizer denied every request when the embedded auth server's upstream provider issues opaque OAuth 2.0 access tokens (Google's ya29.*, GitHub's gho_*). resolveClaims JWT-parsed the upstream token unconditionally and returned the parse error verbatim, so every authorization check failed and the gateway skipped every tool.
  • Discriminate by token shape: if the upstream token is not three dot-separated segments it cannot be a JWT (per JOSE compact serialization), so fall back to identity.Claims (the request-token claims). The embedded auth server already mirrors the upstream OIDC sub/email/name into its issued AS token (see pkg/authserver/server/session/session.go), so policies referencing standard OIDC claims continue to evaluate correctly.
  • JWT-shaped tokens (three segments) that fail to parse still return the error: a tampered or corrupted upstream JWT must not silently degrade to fallback claims. The added looksLikeJWT(tokenStr) = strings.Count(tokenStr, ".") == 2 discriminator keeps that boundary explicit.
  • The auto-binding of PrimaryUpstreamProvider added in Fix Cedar upstream-claim evaluation on VirtualMCPServer #5002 (operator side, cmd/thv-operator/pkg/vmcpconfig/converter.go) doesn't expose an opt-out CRD field, so users with opaque-token providers had no in-config workaround. This change unblocks them without changing operator behavior.

Closes #5146

Type of change

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

Test plan

  • Unit tests (go test ./pkg/authz/...)
  • go vet ./pkg/authz/...
  • gofmt -l pkg/authz/authorizers/cedar/ (clean)
  • End-to-end verified on a GKE staging cluster: VirtualMCPServer with accounts.google.com upstream, Cedar policy referencing claim_email. Pre-fix: tools/list returns empty with Authorization check failed for tool, skipping per tool. Post-fix: full aggregated tool list returned, single WARN line upstream token is not a JWT; falling back to request-token claims for Cedar evaluation provider=google.

The existing TestParseUpstreamJWTClaims/opaque_token_returns_error row stays unchanged — the helper still returns the same error; only the caller's recovery behavior changes.

API Compatibility

  • This PR does not break the v1beta1 API. No CRD or ConfigOptions schema change.

Changes

File Change
pkg/authz/authorizers/cedar/core.go resolveClaims discriminates looksLikeJWT(tokenStr) before falling back. Opaque (≠3 segments) → fall back to identity.Claims with a WARN. JWT-shaped but unparseable → return the original parse error (deny). Adds the looksLikeJWT helper.
pkg/authz/authorizers/cedar/core_test.go Replaces the upstream_token_opaque_not_parseable row in TestAuthorizeWithJWTClaims_UpstreamProvider with three rows: opaque-fallback-denied (policy mismatch via fallback claims), opaque-fallback-permitted (policy matches via fallback claims), and JWT-shaped-but-malformed-still-errors (security-regression guard for tampered JWTs).

Does this introduce a user-facing change?

Yes — bug-fix only. Operators with Google (or any opaque-access-token) upstream OIDC provider now receive the aggregated tools list instead of an empty one. No CRD or configuration change required. A WARN log surfaces each fallback so operators can observe the path their identity claims take.

Tampered or corrupted upstream JWTs still hard-deny — there is no behavior change for JWT-access-token providers (Okta, Azure AD with JWT access tokens, etc.).

Special notes for reviewers

A complementary operator-side change — only auto-set PrimaryUpstreamProvider when the user hasn't asked for the explicit fallback — would expose the documented escape hatch via a CRD field, since the docstring on PrimaryUpstreamProvider already says "When empty, claims from the ToolHive-issued token are used." That route is more invasive (CRD + types + converter + tests). Happy to PR it separately if you'd prefer the opt-in surface over the implicit fallback in this PR. Either approach unblocks the failing case.

The fallback could also be argued to belong behind a ConfigOptions.AllowOpaqueTokenFallback knob (default false). I left it implicit here because (a) the discriminator preserves the deny for tampered JWTs, which is the security-relevant case, and (b) for opaque-token providers the previous behavior was always-deny — there's no operator who relied on it doing anything useful. Open to adding the gate if you'd prefer the more conservative surface.

jhrozek
jhrozek previously approved these changes May 13, 2026
Comment thread pkg/authz/authorizers/cedar/core.go
@github-actions github-actions Bot added the size/XS Extra small PR: < 100 lines changed label May 13, 2026
@jhrozek

jhrozek commented May 13, 2026

Copy link
Copy Markdown
Contributor

hey @cjohnhanson first of all sorry it took so long to review your PR, it somehow slipped through the review queue. That shouldn't happen!

I've included just one nit inline, let me know if you want to address it if not, I'm fine sending a follow up (feeling a bit bad to ask you to do more work after letting you wait for almost 2 weeks..)

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.55556% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.82%. Comparing base (394b2e7) to head (dc40547).
⚠️ Report is 15 commits behind head on main.

Files with missing lines Patch % Lines
pkg/authz/authorizers/cedar/core.go 55.55% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5147      +/-   ##
==========================================
- Coverage   68.83%   68.82%   -0.01%     
==========================================
  Files         627      627              
  Lines       63590    63599       +9     
==========================================
  Hits        43772    43772              
- Misses      16568    16575       +7     
- Partials     3250     3252       +2     

☔ View full report in Codecov by Sentry.
📢 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.

@jhrozek

jhrozek commented May 13, 2026

Copy link
Copy Markdown
Contributor

oh looks like the linter is unhappy as well, this might require another push

VirtualMCPServer (Cedar incoming authz) denied every request when the
embedded auth servers upstream provider issues opaque OAuth 2.0 access
tokens (Googles ya29.*, GitHubs gho_*). resolveClaims tried to JWT-parse
the upstream token unconditionally and returned the parse error verbatim,
so every authorization check failed and the gateway skipped every tool.

Discriminate by token shape: if the upstream token is not three dot-
separated segments it cannot be a JWT, so fall back to identity.Claims
(the request-token claims). The embedded auth server already mirrors
the upstream OIDC sub, email and name into its issued AS token (see
pkg/authserver/server/session/session.go), so policies referencing
standard OIDC claims continue to evaluate correctly.

JWT-shaped tokens (three segments) that fail to parse still return the
error: a tampered or corrupted upstream JWT must not silently degrade
to fallback claims.

Closes stacklok#5146

Signed-off-by: Cody J. Hanson <cjohnhanson@users.noreply.github.com>
@cjohnhanson

Copy link
Copy Markdown
Author

@jhrozek no worries, sorry for the delay on coming back to this. Force-pushed with the nit addressed.

@github-actions github-actions Bot added size/XS Extra small PR: < 100 lines changed and removed size/XS Extra small PR: < 100 lines changed labels Jun 1, 2026
@jhrozek jhrozek merged commit bfde12d into stacklok:main Jun 1, 2026
79 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XS Extra small PR: < 100 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VirtualMCPServer Cedar authorization rejects all requests when upstream OIDC provider issues opaque access tokens

2 participants