Skip to main content

Authentication

Virtual MCP Server (vMCP) implements a two-boundary authentication model that separates client and backend authentication, giving you centralized control over access while supporting diverse backend requirements.

Two-boundary authentication model

Boundary 1 (Incoming): Clients authenticate to vMCP using OAuth 2.1 authorization as defined in the MCP specification. This is your organization's identity layer.

Boundary 2 (Outgoing): vMCP obtains appropriate credentials for each backend. Each backend API receives a token or credential scoped to its requirements.

Incoming authentication

Configure how clients authenticate to vMCP.

Anonymous (development only)

No authentication required:

VirtualMCPServer resource
spec:
incomingAuth:
type: anonymous
warning

Do not use anonymous authentication in production environments. This setting disables all access control, allowing anyone to use the vMCP without credentials.

OIDC authentication

Validate tokens from an external identity provider:

VirtualMCPServer resource
spec:
incomingAuth:
type: oidc
oidcConfig:
type: inline
inline:
issuer: https://auth.example.com
clientId: <YOUR_CLIENT_ID>
audience: vmcp

When using an identity provider that issues opaque OAuth tokens, add a clientSecretRef referencing a Kubernetes Secret to enable token introspection:

VirtualMCPServer resource
spec:
incomingAuth:
type: oidc
oidcConfig:
type: inline
inline:
issuer: https://auth.example.com
clientId: <YOUR_CLIENT_ID>
audience: vmcp
clientSecretRef:
name: oidc-client-secret
key: clientSecret

Create the Secret:

apiVersion: v1
kind: Secret
metadata:
name: oidc-client-secret
namespace: toolhive-system
type: Opaque
stringData:
clientSecret: <YOUR_CLIENT_SECRET>

Kubernetes service account tokens

Authenticate using Kubernetes service account tokens for in-cluster clients:

VirtualMCPServer resource
spec:
incomingAuth:
type: oidc
oidcConfig:
type: kubernetes
kubernetes:
audience: toolhive

This configuration uses the Kubernetes API server as the OIDC issuer and validates service account tokens. The defaults work for most clusters:

  • issuer: https://kubernetes.default.svc (auto-detected)
  • audience: toolhive (configurable)

Outgoing authentication

Configure how vMCP authenticates to backend MCP servers.

Discovery mode

When using discovery mode, vMCP checks each backend MCPServer's externalAuthConfigRef to determine how to authenticate. If a backend has no auth config, vMCP connects without authentication.

VirtualMCPServer resource
spec:
outgoingAuth:
source: discovered

This is the recommended approach for most deployments. Backends that don't require authentication work automatically, while backends with externalAuthConfigRef configured use their specified authentication method.

See Configure token exchange for backend authentication for details on using service account token exchange for backend authentication.

Upstream token injection

The upstreamInject outgoing auth strategy injects a user's upstream access token into outgoing requests to a backend. Unlike other strategies that use static credentials or token exchange, upstream token injection reads tokens that the embedded authorization server acquired during the user's interactive login.

Create an MCPExternalAuthConfig resource with the upstreamInject type. The providerName must match an upstream provider configured on the embedded authorization server:

MCPExternalAuthConfig resource
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPExternalAuthConfig
metadata:
name: inject-github
namespace: toolhive-system
spec:
type: upstreamInject
upstreamInject:
providerName: github

Then reference it in the VirtualMCPServer's outgoing auth configuration:

VirtualMCPServer resource
spec:
outgoingAuth:
source: inline
backends:
backend-github:
externalAuthConfigRef:
name: inject-github

When a request reaches the backend-github MCPServer, vMCP replaces the Authorization header with the upstream access token stored for the github provider during the user's login flow. Backends not listed in the backends map receive unauthenticated requests.

note

Upstream token injection requires an embedded authorization server configured on the VirtualMCPServer. The providerName must match a provider name in the auth server's upstreamProviders list.

Token exchange with upstream tokens

You can combine the embedded authorization server with token exchange by adding the subjectProviderName field to a tokenExchange config. This tells the token exchange middleware to use the stored upstream token from the named provider as the subject token for the RFC 8693 exchange, instead of the vMCP-issued JWT.

This is useful when a backend needs a token exchanged at the same identity provider that issued the upstream token. For example, if the embedded auth server acquires an Okta access token during login, you can exchange that token at a different Okta authorization server for a backend-scoped token:

MCPExternalAuthConfig resource
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPExternalAuthConfig
metadata:
name: exchange-okta
namespace: toolhive-system
spec:
type: tokenExchange
tokenExchange:
tokenUrl: https://<YOUR_OKTA_DOMAIN>/oauth2/<AUTH_SERVER_ID>/v1/token
clientId: <YOUR_CLIENT_ID>
clientSecretRef:
name: okta-exchange-client-secret
key: client-secret
audience: backend
scopes:
- backend-api:read
subjectProviderName: okta

Without subjectProviderName, token exchange uses the vMCP-issued JWT as the subject token. With it, the exchange uses the raw upstream provider's access token, which the exchange endpoint can validate directly.

You can mix both strategies in the same vMCP deployment. For example, some backends can use upstreamInject for direct token forwarding while others use tokenExchange with subjectProviderName for exchanged tokens:

VirtualMCPServer resource
spec:
outgoingAuth:
source: inline
backends:
backend-github:
externalAuthConfigRef:
name: inject-github
backend-okta-app:
externalAuthConfigRef:
name: exchange-okta

Embedded authorization server

The embedded authorization server runs an OAuth authorization server within the vMCP process. It redirects users to one or more upstream identity providers (such as GitHub, Google, or Okta) for interactive authentication, stores the upstream tokens, and issues its own JWTs that the vMCP validates on subsequent requests. Combined with upstream token injection or token exchange with upstream tokens, this bridges both authentication boundaries: the auth server handles incoming auth by issuing JWTs, while the outgoing strategies forward or exchange the stored upstream tokens for backends.

Use the embedded authorization server when your backend MCP servers call external APIs on behalf of individual users and no federation relationship exists between your identity provider and those services. It also provides OAuth 2.0 Dynamic Client Registration (RFC 7591), so MCP clients can register automatically without manual client configuration in ToolHive.

info

For conceptual background on the embedded authorization server including token storage and forwarding, see Backend authentication concepts. For configuring the embedded auth server on individual MCPServer resources (single upstream provider), see Set up embedded authorization server authentication.

How it works

When multiple upstream providers are configured, the auth server chains authorization flows sequentially. The user is redirected to each provider in order, and the auth server stores each provider's tokens before moving to the next. After the final provider completes, the auth server issues a single JWT to the client.

Differences from MCPServer embedded auth server

The embedded auth server uses the same configuration structure whether on a VirtualMCPServer or an MCPServer. The key differences for vMCP are:

  • Inline configuration: The auth server config lives directly on the VirtualMCPServer resource under authServerConfig, rather than in a separate MCPExternalAuthConfig resource.
  • Multiple upstream providers: vMCP supports multiple upstream providers with sequential authorization chaining. MCPServer is limited to a single upstream provider.
  • Flexible outgoing strategies: vMCP uses upstreamInject or tokenExchange with subjectProviderName to route stored tokens to the correct backends. MCPServer swaps the token automatically because it has a single upstream provider.

Configure the embedded auth server

Add an authServerConfig block to your VirtualMCPServer. The configuration fields are the same as for the MCPServer embedded auth server -- see that guide for generating keys and creating Secrets.

VirtualMCPServer resource
spec:
authServerConfig:
issuer: https://auth.example.com
signingKeySecretRefs:
- name: auth-signing-key
key: private-key
hmacSecretRefs:
- name: auth-hmac-key
key: hmac-key
tokenLifespans:
accessTokenLifespan: 1h
refreshTokenLifespan: 168h
authCodeLifespan: 10m
upstreamProviders:
- name: github
type: oauth2
oauth2Config: { ... }
- name: google
type: oidc
oidcConfig: { ... }

Each upstream provider name must be a valid DNS label (lowercase alphanumeric and hyphens, max 63 characters). This name is what upstream token injection and token exchange configs reference to map backends to providers. For details on configuring OIDC vs OAuth 2.0 upstream providers, see Using an OAuth 2.0 upstream provider. The complete example below shows full provider configurations.

Incoming auth with the embedded auth server

When using the embedded auth server, configure incomingAuth to validate the JWTs it issues. The issuer must match authServerConfig.issuer, and jwksAllowPrivateIP must be true because the vMCP validates tokens from its own in-process auth server via loopback:

VirtualMCPServer resource
spec:
incomingAuth:
type: oidc
resourceUrl: https://mcp.example.com/mcp
oidcConfig:
type: inline
inline:
issuer: https://auth.example.com
audience: https://mcp.example.com/mcp
jwksAllowPrivateIP: true

The resourceUrl is the externally reachable URL of the MCP endpoint. MCP clients use this for RFC 9728 protected resource metadata discovery to find the authorization server.

Session storage

By default, upstream tokens are stored in memory and lost on pod restart. For production, configure Redis Sentinel by adding a storage block to authServerConfig. The configuration is the same as for the MCPServer embedded auth server. See Redis Sentinel session storage for a complete walkthrough.

Complete example

This example deploys a vMCP with an embedded auth server that authenticates users through GitHub and Google, then injects the GitHub access token into requests to a GitHub MCP server backend.

Prerequisites: Create Secrets for signing keys, HMAC keys, and upstream provider credentials following the steps in Set up embedded authorization server authentication. You need: auth-signing-key, auth-hmac-key, github-client-secret, and google-client-secret.

Step 1: Create an MCPGroup and deploy the backend MCP server:

backends.yaml
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPGroup
metadata:
name: my-backends
namespace: toolhive-system
---
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPServer
metadata:
name: backend-github
namespace: toolhive-system
spec:
image: ghcr.io/github/github-mcp-server
transport: streamable-http
groupRef: my-backends

Step 2: Create the upstream token injection config:

auth-configs.yaml
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPExternalAuthConfig
metadata:
name: inject-github
namespace: toolhive-system
spec:
type: upstreamInject
upstreamInject:
providerName: github

Step 3: Deploy the VirtualMCPServer:

virtualmcpserver.yaml
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: VirtualMCPServer
metadata:
name: my-vmcp
namespace: toolhive-system
spec:
config:
groupRef: my-backends
authServerConfig:
issuer: https://auth.example.com
signingKeySecretRefs:
- name: auth-signing-key
key: private-key
hmacSecretRefs:
- name: auth-hmac-key
key: hmac-key
tokenLifespans:
accessTokenLifespan: 1h
refreshTokenLifespan: 168h
authCodeLifespan: 10m
upstreamProviders:
- name: github
type: oauth2
oauth2Config:
authorizationEndpoint: https://github.com/login/oauth/authorize
tokenEndpoint: https://github.com/login/oauth/access_token
clientId: <YOUR_GITHUB_CLIENT_ID>
clientSecretRef:
name: github-client-secret
key: client-secret
scopes:
- repo
- read:user
userInfo:
endpointUrl: https://api.github.com/user
httpMethod: GET
additionalHeaders:
Accept: application/vnd.github+json
fieldMapping:
subjectFields:
- id
- login
nameFields:
- name
- login
emailFields:
- email
- name: google
type: oidc
oidcConfig:
issuerUrl: https://accounts.google.com
clientId: <YOUR_GOOGLE_CLIENT_ID>
clientSecretRef:
name: google-client-secret
key: client-secret
scopes:
- openid
- email
incomingAuth:
type: oidc
resourceUrl: https://mcp.example.com/mcp
oidcConfig:
type: inline
inline:
issuer: https://auth.example.com
audience: https://mcp.example.com/mcp
jwksAllowPrivateIP: true
outgoingAuth:
source: inline
backends:
backend-github:
externalAuthConfigRef:
name: inject-github

Step 4: Verify the deployment:

# Check the VirtualMCPServer status
kubectl get virtualmcpserver -n toolhive-system my-vmcp

# Verify OAuth discovery is available
curl https://auth.example.com/.well-known/oauth-authorization-server

Connect with an MCP client that supports the MCP authorization specification. The client discovers the authorization server through protected resource metadata, then redirects you through each upstream provider for authentication. After completing the login flow, MCP tool calls to the GitHub backend automatically include your GitHub access token.