You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a VirtualMCPServer is configured with an embedded auth server using an upstream OIDC provider whose access tokens are opaque rather than JWTs (Google issues ya29.* opaque tokens), every Cedar authorization check fails. The vMCP gateway logs Authorization check failed for tool, skipping for every tool and returns an empty tools/list to authenticated clients.
#5002 introduced the PrimaryUpstreamProvider auto-binding for VirtualMCPServer that mirrors the working thv run path (verified in that PR against Okta, whose access tokens are JWTs). The same auto-binding causes the JWT parse step to fail unconditionally for OIDC providers that issue opaque access tokens.
Reproduction
Minimal VirtualMCPServer with Google upstream + a permit-everything Cedar policy:
Authenticate via the embedded auth server's Google OAuth flow.
Connect an MCP client (Claude Desktop, claude.ai, etc.) and call tools/list.
Expected behavior
The empty Cedar policy permit(principal, action, resource); permits all tools, and the client receives the aggregated tool list.
Actual behavior
Every tool is skipped with this gateway log line:
Authorization check failed for tool, skipping
tool: <tool-name>
error: failed to parse upstream token for provider "google":
upstream token is not a parseable JWT: token is malformed:
token contains an invalid number of segments
The client receives an empty tool list and cannot use the gateway.
Root cause
Verified against f9489a4b88cfdf6f984d4f665e00a01e90f13dfc (current main plus PR #4994):
Operator auto-binds without an opt-out.cmd/thv-operator/pkg/vmcpconfig/converter.go:201-205 unconditionally sets incoming.Authz.PrimaryUpstreamProvider to the first upstream provider's name whenever authServerConfig.upstreamProviders is non-empty. There is no CRD field exposed to override this.
Storage holds the ID token but the reader returns only the access token.pkg/authserver/storage/types.go defines UpstreamTokens with both AccessToken (line 61) and IDToken (line 63) fields. The OAuth callback at pkg/authserver/server/handlers/callback.go:131-140 populates both from idpTokens. pkg/auth/upstreamtoken/service.go (lines 81, 107, 122, 174) returns tokens.AccessToken only. IDToken is never plumbed into UpstreamCredential or identity.UpstreamTokens.
Cedar parse has no fallback.pkg/authz/authorizers/cedar/core.go:421-443 calls parseUpstreamJWTClaims(upstreamToken) whenever primaryUpstreamProvider != "", regardless of whether the policy text references upstream-prefixed claims. The function comment at line 461 explicitly notes "Returns an error if the token is not a parseable JWT (e.g. opaque token)" — but the caller returns the error directly with no fallback path.
The pkg/authserver/server/session/session.go JWT session builder already maps the upstream email/name/sub into the AS-issued JWT's claims (around lines 135-138), so falling back to identity.Claims produces the same authorization decisions for the common case where policies reference the standard OIDC claims.
Proposed fix
Smallest defensible change: in resolveClaims, when parseUpstreamJWTClaims returns the "not a parseable JWT" error, log a WARN and fall back to jwt.MapClaims(identity.Claims). Preserves Okta and other JWT-access-token providers (no behavior change) while letting opaque-access-token providers like Google reuse the AS-issued token's claims.
A complementary operator 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. Not required for the immediate bug fix but worth pairing.
Workarounds today
incomingAuth.authzConfig.type: "none" disables Cedar entirely.
Removing authServerConfig.upstreamProviders and pointing incomingAuth.oidcConfigRef directly at Google as the OIDC issuer keeps Cedar functional but loses the embedded-AS features (token persistence, server-side token exchange to backends).
Confirmed against v0.26.1 stock images carrying the same converter and Cedar code.
Intent to contribute
Planning to submit a PR with the Cedar fallback plus a unit test covering the opaque-upstream-token path. Happy to also pair the operator opt-out change if that's of interest.
Summary
When a
VirtualMCPServeris configured with an embedded auth server using an upstream OIDC provider whose access tokens are opaque rather than JWTs (Google issuesya29.*opaque tokens), every Cedar authorization check fails. The vMCP gateway logsAuthorization check failed for tool, skippingfor every tool and returns an emptytools/listto authenticated clients.#5002 introduced the
PrimaryUpstreamProviderauto-binding for VirtualMCPServer that mirrors the workingthv runpath (verified in that PR against Okta, whose access tokens are JWTs). The same auto-binding causes the JWT parse step to fail unconditionally for OIDC providers that issue opaque access tokens.Reproduction
Minimal
VirtualMCPServerwith Google upstream + a permit-everything Cedar policy:MCPOIDCConfigand Secret resources, on v0.21.0 or later (any release containing Fix Cedar upstream-claim evaluation on VirtualMCPServer #5002).tools/list.Expected behavior
The empty Cedar policy
permit(principal, action, resource);permits all tools, and the client receives the aggregated tool list.Actual behavior
Every tool is skipped with this gateway log line:
The client receives an empty tool list and cannot use the gateway.
Root cause
Verified against
f9489a4b88cfdf6f984d4f665e00a01e90f13dfc(currentmainplus PR #4994):Operator auto-binds without an opt-out.
cmd/thv-operator/pkg/vmcpconfig/converter.go:201-205unconditionally setsincoming.Authz.PrimaryUpstreamProviderto the first upstream provider's name wheneverauthServerConfig.upstreamProvidersis non-empty. There is no CRD field exposed to override this.Storage holds the ID token but the reader returns only the access token.
pkg/authserver/storage/types.godefinesUpstreamTokenswith bothAccessToken(line 61) andIDToken(line 63) fields. The OAuth callback atpkg/authserver/server/handlers/callback.go:131-140populates both fromidpTokens.pkg/auth/upstreamtoken/service.go(lines 81, 107, 122, 174) returnstokens.AccessTokenonly.IDTokenis never plumbed intoUpstreamCredentialoridentity.UpstreamTokens.Cedar parse has no fallback.
pkg/authz/authorizers/cedar/core.go:421-443callsparseUpstreamJWTClaims(upstreamToken)wheneverprimaryUpstreamProvider != "", regardless of whether the policy text references upstream-prefixed claims. The function comment at line 461 explicitly notes "Returns an error if the token is not a parseable JWT (e.g. opaque token)" — but the caller returns the error directly with no fallback path.The
pkg/authserver/server/session/session.goJWT session builder already maps the upstreamemail/name/subinto the AS-issued JWT's claims (around lines 135-138), so falling back toidentity.Claimsproduces the same authorization decisions for the common case where policies reference the standard OIDC claims.Proposed fix
Smallest defensible change: in
resolveClaims, whenparseUpstreamJWTClaimsreturns the "not a parseable JWT" error, log a WARN and fall back tojwt.MapClaims(identity.Claims). Preserves Okta and other JWT-access-token providers (no behavior change) while letting opaque-access-token providers like Google reuse the AS-issued token's claims.A complementary operator change — only auto-set
PrimaryUpstreamProviderwhen the user hasn't asked for the explicit fallback — would expose the documented escape hatch via a CRD field. Not required for the immediate bug fix but worth pairing.Workarounds today
incomingAuth.authzConfig.type: "none"disables Cedar entirely.authServerConfig.upstreamProvidersand pointingincomingAuth.oidcConfigRefdirectly at Google as the OIDC issuer keeps Cedar functional but loses the embedded-AS features (token persistence, server-side token exchange to backends).Environment
f9489a4b88cfdf6f984d4f665e00a01e90f13dfc(currentmain+ PR Allow standalone Redis in auth server storage #4994).Intent to contribute
Planning to submit a PR with the Cedar fallback plus a unit test covering the opaque-upstream-token path. Happy to also pair the operator opt-out change if that's of interest.