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
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
- For
thv proxy
command reference, seethv proxy