Authentication and authorization
This guide shows you how to secure your MCP servers using OAuth-based authentication and Cedar-based authorization policies with the ToolHive CLI.
Authentication and authorization are emerging capabilities in the MCP ecosystem. The official MCP authorization specification is still evolving, and client support for these features is limited. ToolHive is leading the way in implementing these capabilities, but you may encounter some limitations with certain clients.
Prerequisites
Before you begin, make sure you have:
- ToolHive installed and working
- Basic familiarity with OAuth, OIDC, and JWT concepts
- An identity provider that supports OpenID Connect (OIDC), such as Google, GitHub, Microsoft Entra ID (Azure AD), Okta, Auth0, or Kubernetes (for service accounts)
From your identity provider, you'll need:
- Client ID
- Audience value
- Issuer URL
- JWKS URL (for key verification)
ToolHive uses OIDC to connect to your existing identity provider, so you can authenticate with your own credentials (for example, Google login) or with service account tokens (for example, in Kubernetes). ToolHive never sees your password, only signed tokens from your identity provider.
For background on authentication, authorization, and Cedar policy examples, see Authentication and authorization framework.
Set up authentication
Step 1: Gather OIDC configuration
First, collect the necessary information from your identity provider:
- Client ID
- Audience value
- Issuer URL
- JWKS URL (for key verification)
Step 2: Run an MCP server with authentication
Use the following command to start an MCP server with authentication enabled:
thv run \
--oidc-audience <your-audience> \
--oidc-client-id <your-client-id> \
--oidc-issuer <https://your-oidc-issuer.com> \
--oidc-jwks-url <https://your-oidc-issuer.com/path/to/jwks> \
<server-name>
Replace the placeholders with your actual OIDC configuration.
Step 3: Test authentication
Once your server is running with authentication enabled, clients must include a
valid JWT (JSON Web Token) in the Authorization
header of each HTTP request.
The token should:
- Be issued by your configured identity provider
- Include the correct audience claim
- Not be expired
- Have a valid signature
:::note Client support limitations
Client support for authentication is limited at this time. While some clients support HTTP headers with SSE MCP client configurations, we do not recommend passing JWT tokens in this way since it requires manual configuration and exposes your token in plain text.
We are working on a solution within ToolHive to securely handle authentication for clients. Stay tuned for updates on this feature.
:::
:::note Obtaining JWT tokens
How to obtain JWT tokens varies by identity provider and is outside the scope of this guide. Consult your identity provider's documentation for specific instructions on:
- Interactive user authentication flows (OAuth 2.0 Authorization Code flow)
- Service-to-service authentication (Client Credentials flow)
- API token generation and management
For Kubernetes service accounts, tokens are automatically mounted at
/var/run/secrets/kubernetes.io/serviceaccount/token
in pods.
:::
To verify that authentication is working, you can use a tool like curl
to make
a request to your MCP server:
curl -H "Authorization: Bearer <your-jwt-token>" \
<toolhive-server-url>
Set up proxy authentication
ToolHive provides a standalone thv proxy
command that creates transparent HTTP
proxies with advanced authentication capabilities. This is different from using
thv run
with authentication—the proxy command creates a standalone proxy
process without managing workloads or containers.
When to use proxy authentication
Use the thv proxy
command when you need:
- Outgoing authentication: Authenticate to remote MCP servers using OAuth/OIDC, with the proxy handling token management and refresh
- Incoming authentication: Protect a proxy endpoint with OIDC validation, requiring clients to provide valid JWT tokens
- Bidirectional authentication: Secure both incoming requests to the proxy and outgoing requests to remote servers
- Dynamic client registration: Automatically register OAuth clients using RFC 7591, eliminating the need for pre-configuration
- Standalone proxy: Run a proxy without creating a managed workload
Basic proxy with outgoing authentication
To create a proxy that authenticates to a remote MCP server:
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-issuer https://auth.example.com \
--remote-auth-client-id my-client-id \
--remote-auth-client-secret-file /path/to/secret
The proxy will:
- Handle the OAuth/OIDC flow with the remote server
- Manage token storage and automatic refresh
- Forward authenticated requests transparently
Your clients connect to the local proxy without needing to handle authentication.
Proxy with incoming authentication
To protect the proxy endpoint itself with OIDC validation:
thv proxy my-server \
--target-uri http://localhost:8080 \
--oidc-issuer https://auth.example.com \
--oidc-audience my-audience \
--oidc-client-id my-client-id
Clients must include a valid JWT token when connecting to the proxy.
Bidirectional proxy authentication
Combine both incoming and outgoing authentication for end-to-end security:
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-issuer https://remote-auth.example.com \
--remote-auth-client-id remote-client-id \
--remote-auth-client-secret-file /path/to/remote-secret \
--oidc-issuer https://local-auth.example.com \
--oidc-audience my-audience \
--oidc-client-id local-client-id
Authentication modes
The thv proxy
command supports four authentication scenarios:
- No authentication: Simple transparent forwarding
- Outgoing authentication: Authenticate to remote MCP servers using OAuth/OIDC
- Incoming authentication: Protect the proxy endpoint with OIDC validation
- Bidirectional: Both incoming and outgoing authentication
OAuth2 authentication (non-OIDC)
For non-OIDC OAuth2 servers, specify the authorization and token endpoints explicitly:
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-authorize-url https://auth.example.com/oauth/authorize \
--remote-auth-token-url https://auth.example.com/oauth/token \
--remote-auth-client-id my-client-id \
--remote-auth-client-secret-file /path/to/secret
Auto-detect authentication
The proxy can automatically detect if a remote server requires authentication by examining WWW-Authenticate headers:
thv proxy my-server \
--target-uri https://protected-api.com \
--remote-auth-client-id my-client-id
When authentication is detected, the proxy automatically initiates the appropriate OAuth flow.
Dynamic client registration
When no client credentials are provided, the proxy can automatically register an OAuth client using RFC 7591 dynamic client registration:
thv proxy my-server \
--target-uri https://protected-api.com \
--remote-auth \
--remote-auth-issuer https://auth.example.com
This feature:
- Eliminates the need to pre-configure OAuth clients
- Automatically discovers the registration endpoint via OIDC
- Supports PKCE flow for enhanced security
Dynamic client registration requires the remote authorization server to support RFC 7591. Not all OAuth providers support this feature.
Token introspection
For advanced scenarios, you can use token introspection for incoming authentication:
thv proxy my-server \
--target-uri http://localhost:8080 \
--oidc-issuer https://auth.example.com \
--oidc-audience my-audience \
--oidc-client-id my-client-id \
--oidc-client-secret my-client-secret \
--oidc-introspection-url https://auth.example.com/oauth/introspect
Advanced proxy configuration
Custom OAuth scopes
Specify the OAuth scopes required by the remote server:
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-issuer https://auth.example.com \
--remote-auth-client-id my-client-id \
--remote-auth-client-secret-file /path/to/secret \
--remote-auth-scopes openid,profile,email,custom-scope
If --remote-auth-scopes
is not specified, OIDC authentication defaults to
openid,profile,email
.
Authentication timeout
Adjust the timeout for slow networks or interactive authentication flows:
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-issuer https://auth.example.com \
--remote-auth-client-id my-client-id \
--remote-auth-client-secret-file /path/to/secret \
--remote-auth-timeout 2m
Headless authentication
For non-interactive environments (CI/CD, servers), skip browser-based OAuth flows:
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-issuer https://auth.example.com \
--remote-auth-client-id my-client-id \
--remote-auth-client-secret-file /path/to/secret \
--remote-auth-skip-browser
This requires client credentials flow or pre-authorized tokens.
Custom proxy host and port
By default, the proxy listens on 127.0.0.1
with a random port. To specify
custom values:
thv proxy my-server \
--target-uri http://localhost:8080 \
--host 0.0.0.0 \
--port 8000
Custom OAuth callback port
The proxy uses a temporary HTTP server for OAuth callbacks during authentication. To specify a custom callback port:
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-issuer https://auth.example.com \
--remote-auth-client-id my-client-id \
--remote-auth-client-secret-file /path/to/secret \
--remote-auth-callback-port 9000
The default callback port is 8666
.
Credential management
Client secret sources
OAuth client secrets can be provided via three methods (in order of precedence):
--remote-auth-client-secret
flag (not recommended for production)--remote-auth-client-secret-file
flag (recommended)TOOLHIVE_REMOTE_OAUTH_CLIENT_SECRET
environment variable
Always use --remote-auth-client-secret-file
instead of
--remote-auth-client-secret
in production environments. The file-based
approach prevents credentials from appearing in process lists or command
history.
Using environment variables
export TOOLHIVE_REMOTE_OAUTH_CLIENT_SECRET="your-secret-here"
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-issuer https://auth.example.com \
--remote-auth-client-id my-client-id
Using secret files
echo "your-secret-here" > /secure/path/secret.txt
chmod 600 /secure/path/secret.txt
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-issuer https://auth.example.com \
--remote-auth-client-id my-client-id \
--remote-auth-client-secret-file /secure/path/secret.txt
Common proxy use cases
Protecting a local MCP server
Run a local MCP server and protect it with OIDC authentication:
# Start your MCP server on localhost:8080
# Then create a protected proxy
thv proxy protected-mcp \
--target-uri http://localhost:8080 \
--port 3000 \
--oidc-issuer https://auth.example.com \
--oidc-audience mcp-users \
--oidc-client-id mcp-proxy
Clients now connect to http://localhost:3000
with valid JWT tokens.
Accessing a protected remote MCP server
Connect to a remote MCP server that requires OAuth authentication:
thv proxy remote-mcp \
--target-uri https://api.example.com/mcp \
--port 3000 \
--remote-auth \
--remote-auth-issuer https://auth.example.com \
--remote-auth-client-id your-client-id \
--remote-auth-client-secret-file /path/to/secret
Clients connect to http://localhost:3000
without handling OAuth flows.
Creating a secure gateway
Set up a secure gateway with authentication on both sides:
thv proxy secure-gateway \
--target-uri https://api.example.com/mcp \
--port 3000 \
--remote-auth \
--remote-auth-issuer https://remote.example.com \
--remote-auth-client-id remote-id \
--remote-auth-client-secret-file /path/to/remote-secret \
--oidc-issuer https://local.example.com \
--oidc-audience gateway-users \
--oidc-client-id gateway-proxy
Token exchange (RFC 8693)
The thv proxy
command supports OAuth 2.0 Token Exchange (RFC 8693), which
allows you to securely exchange one token for another. This is useful when you
need to obtain downstream tokens with different audiences, scopes, or token
types.
When to use token exchange
Token exchange is valuable in scenarios where:
- You have a user's authentication token but need a different token to access a backend service
- You need to exchange an ID token for an access token (or vice versa)
- You're working with Google Cloud Workload Identity Federation or similar services
- You need to obtain tokens with specific audiences or scopes for downstream services
Basic token exchange setup
To enable token exchange with the proxy:
thv proxy my-server \
--target-uri https://api.example.com \
--token-exchange-url https://auth.example.com/oauth/token \
--token-exchange-client-id exchange-client-id \
--token-exchange-client-secret-file /path/to/exchange-secret
The proxy will:
- Extract the token from incoming requests (Authorization header)
- Exchange it for a new token using the token exchange endpoint
- Forward requests with the exchanged token to the target server
Token exchange configuration
Subject token type
Specify the type of token to exchange using
--token-exchange-subject-token-type
:
thv proxy my-server \
--target-uri https://api.example.com \
--token-exchange-url https://auth.example.com/oauth/token \
--token-exchange-client-id exchange-client-id \
--token-exchange-client-secret-file /path/to/exchange-secret \
--token-exchange-subject-token-type id_token
Supported values:
access_token
: Exchange an access tokenid_token
: Exchange an ID token
Target audience
Specify the audience for the exchanged token:
thv proxy my-server \
--target-uri https://api.example.com \
--token-exchange-url https://auth.example.com/oauth/token \
--token-exchange-client-id exchange-client-id \
--token-exchange-client-secret-file /path/to/exchange-secret \
--token-exchange-audience https://backend.example.com
Custom scopes
Request specific scopes for the exchanged token:
thv proxy my-server \
--target-uri https://api.example.com \
--token-exchange-url https://auth.example.com/oauth/token \
--token-exchange-client-id exchange-client-id \
--token-exchange-client-secret-file /path/to/exchange-secret \
--token-exchange-scopes read,write,admin
Custom token header
By default, the exchanged token is injected in the Authorization
header. To
use a custom header:
thv proxy my-server \
--target-uri https://api.example.com \
--token-exchange-url https://auth.example.com/oauth/token \
--token-exchange-client-id exchange-client-id \
--token-exchange-client-secret-file /path/to/exchange-secret \
--token-exchange-header-name X-Custom-Auth
Token exchange with OAuth flow
You can combine token exchange with the proxy's OAuth authentication:
thv proxy my-server \
--target-uri https://api.example.com \
--remote-auth \
--remote-auth-issuer https://auth.example.com \
--remote-auth-client-id auth-client-id \
--remote-auth-client-secret-file /path/to/auth-secret \
--token-exchange-url https://auth.example.com/oauth/token \
--token-exchange-client-id exchange-client-id \
--token-exchange-client-secret-file /path/to/exchange-secret \
--token-exchange-audience https://backend.example.com
In this mode:
- The proxy authenticates using OAuth and obtains tokens
- The proxy exchanges the obtained token for a downstream token
- Requests are forwarded with the exchanged token
Client secret sources
Token exchange client secrets follow the same precedence as other OAuth secrets:
--token-exchange-client-secret
flag (not recommended for production)--token-exchange-client-secret-file
flag (recommended)TOOLHIVE_TOKEN_EXCHANGE_CLIENT_SECRET
environment variable
Example using environment variable:
export TOOLHIVE_TOKEN_EXCHANGE_CLIENT_SECRET="your-exchange-secret"
thv proxy my-server \
--target-uri https://api.example.com \
--token-exchange-url https://auth.example.com/oauth/token \
--token-exchange-client-id exchange-client-id
Token exchange use case: Google Cloud Workload Identity
A common use case is exchanging tokens for Google Cloud Workload Identity Federation:
thv proxy gcp-service \
--target-uri https://api.example.com \
--token-exchange-url https://sts.googleapis.com/v1/token \
--token-exchange-client-id gcp-client-id \
--token-exchange-client-secret-file /path/to/gcp-secret \
--token-exchange-audience //iam.googleapis.com/projects/PROJECT_ID/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID \
--token-exchange-subject-token-type id_token
This configuration exchanges your ID token for a Google Cloud access token that can be used to access GCP services.
Set up authorization
ToolHive uses Amazon's Cedar policy language for fine-grained, secure-by-default authorization. Authorization is explicit: if a request is not explicitly permitted by a policy, it is denied. Deny rules always take precedence over permit rules.
Step 1: Create an authorization configuration file
Create a JSON or YAML file with Cedar policies. Here's an example in JSON format:
{
"version": "1.0",
"type": "cedarv1",
"cedar": {
"policies": [
// Allow everyone to use the weather tool
"permit(principal, action == Action::\"call_tool\", resource == Tool::\"weather\");",
// Restrict admin_tool to a specific user
"permit(principal == Client::\"alice123\", action == Action::\"call_tool\", resource == Tool::\"admin_tool\");",
// Role-based access: only users with the 'premium' role can call any tool
"permit(principal, action == Action::\"call_tool\", resource) when { principal.claim_roles.contains(\"premium\") };",
// Attribute-based: allow calculator tool only for add/subtract operations
"permit(principal, action == Action::\"call_tool\", resource == Tool::\"calculator\") when { resource.arg_operation == \"add\" || resource.arg_operation == \"subtract\" };"
],
"entities_json": "[]"
}
}
You can also define custom resource attributes in entities_json
for per-tool
ownership or sensitivity labels.
For more policy examples and advanced usage, see Cedar policies.
Save this file to a location accessible to ToolHive, such as
/path/to/authz-config.json
.
Step 2: Run an MCP server with authorization
Start your MCP server with the authorization configuration:
thv run \
--authz-config /path/to/authz-config.json \
<server-name>
You can combine this with the authentication parameters from the previous section:
thv run \
--oidc-audience <your-audience> \
--oidc-client-id <your-client-id> \
--oidc-issuer <https://your-oidc-issuer.com> \
--oidc-jwks-url <https://your-oidc-issuer.com/path/to/jwks> \
--authz-config /path/to/authz-config.json \
<server-name>
Step 3: Test authorization
Once your server is running with authorization enabled, clients will be subject to the Cedar policies defined in your configuration file. When a client attempts to perform an action, ToolHive will evaluate the request against the policies. If the request is permitted, the action will proceed; otherwise, it will be denied with a 403 Forbidden response.
Troubleshooting
Authentication issues
If clients can't authenticate:
-
Check that the JWT token is valid and not expired
-
Verify that the audience and issuer match your configuration
-
Ensure the JWKS URL is accessible
-
Check the server logs for specific authentication errors:
thv logs <server-name>
Authorization issues
If authenticated clients are denied access:
- Make sure your Cedar policies explicitly permit the specific action (remember, default deny)
- Check that the principal, action, and resource match what's in your policies (including case and formatting)
- Examine any conditions in your policies to ensure they're satisfied (for example, required JWT claims or tool arguments)
- Remember that Cedar uses a default deny policy—if no policy explicitly permits an action, it will be denied
Troubleshooting tip: If access is denied, check that your policies explicitly permit the action. Cedar uses a default deny model—if no policy matches, the request is denied.
CLI-specific issues
If you're having issues with the CLI:
- Verify that the configuration file path is correct and accessible
- Check that the server name matches an available MCP server
- Ensure all required CLI flags are provided
Related information
- For conceptual understanding, see Authentication and authorization framework
- For detailed Cedar policy syntax, see Cedar policies and the Cedar documentation