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

No one assigned

    Labels

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

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions