Skip to content

vMCP: ensureOIDCDiscovered blocks local key provider when issuer URL is unreachable #4747

@lorr1

Description

@lorr1

Bug description

When the vMCP has an embedded auth server, ValidateToken() fails fatally on OIDC discovery before the in-process key provider is ever consulted. The local key provider (getKeyFromLocalProvider) is correctly wired up and would resolve keys from the embedded auth server's signing keys in-memory, but ensureOIDCDiscovered() is a hard gate in ValidateToken() that runs first and returns an error when the issuer URL doesn't resolve (e.g., inside a cluster where the external-facing hostname isn't routable).

PR #4502 added the local key provider infrastructure for the runner/proxy runner. PR #4526 wired keyProvider into the vMCP OIDC middleware. However, neither PR modified ensureOIDCDiscovered() to be non-fatal when a local key provider is available — so the plumbing is complete but the gate was never relaxed.

Steps to reproduce

  1. Deploy a vMCP with an embedded auth server where incoming_auth.oidc.issuer is set to an external-facing URL (e.g., a tunnel hostname)
  2. The external URL is not resolvable from inside the cluster
  3. Send a request with a valid JWT signed by the embedded auth server
  4. Token validation fails with OIDC discovery failed: ... no such host

Expected behavior

The local key provider (tier 1) should resolve the signing key in-process without requiring HTTP-based OIDC discovery. ensureOIDCDiscovered() should not be a fatal gate when a keyProvider is configured and can satisfy the request.

Actual behavior

ValidateToken() (pkg/auth/token.go:1046) calls ensureOIDCDiscovered(), which attempts an HTTP GET to {issuer}/.well-known/openid-configuration. This fails because the hostname doesn't resolve inside the cluster. The error is returned at line 1047, and getKeyFromJWKS() — which correctly tries getKeyFromLocalProvider() first at line 888 — never executes.

Call chain:

ValidateToken() 
  → ensureOIDCDiscovered()     // issuer is set → HTTP discovery → fails → return error
  → getKeyFromJWKS()           // NEVER REACHED
    → getKeyFromLocalProvider() // NEVER REACHED (would have succeeded)

Additional context

  • Workaround: Setting jwksUrl explicitly in the OIDC config causes ensureOIDCDiscovered() to short-circuit at token.go:769 (v.jwksURL != ""), allowing getKeyFromJWKS()getKeyFromLocalProvider() to proceed.
  • Suggested fix: Make ensureOIDCDiscovered() non-fatal when keyProvider is present. For example, in ValidateToken():
    if err := v.ensureOIDCDiscovered(ctx); err != nil && v.keyProvider == nil {
        return nil, fmt.Errorf("OIDC discovery failed: %w", err)
    }
  • The runner path avoids this because it may not set the issuer when the embedded auth server is present, so ensureOIDCDiscovered() is a no-op (short-circuits at line 761).
  • Related PRs: Resolve JWKS keys in-process for embedded auth server (MCP server) #4502 (added local key provider for runner), Wire in-process JWKS key resolution for vMCP embedded auth server #4526 (wired keyProvider into vMCP)

Metadata

Metadata

Assignees

Labels

authenticationbugSomething isn't workinggoPull requests that update go codevmcpVirtual MCP Server related issues

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions