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:
spec:
incomingAuth:
type: anonymous
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:
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:
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:
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.
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:
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:
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.
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:
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:
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.
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 separateMCPExternalAuthConfigresource. - 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
upstreamInjectortokenExchangewithsubjectProviderNameto 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.
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:
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:
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:
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:
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.