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.
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)
If you're connecting to an external identity provider, the only value you need
from it is the issuer URL. ToolHive uses it to fetch the JWKS automatically
via OIDC discovery (${issuerUrl}/.well-known/openid-configuration), so you
don't need to look up the JWKS URL yourself unless your provider doesn't support
discovery.
Kubernetes service accounts don't need any of this. ToolHive validates those tokens against the cluster's own OIDC issuer and keys automatically.
Two more values matter, but neither is always required:
- Audience: a value you choose to identify this MCP server, not something
the provider generates. ToolHive checks that the token's
audclaim matches it; some providers let you register the value so they include it in issued tokens, but ToolHive doesn't require that. - Client ID and secret: only needed if your provider validates tokens by introspection rather than issuing JWTs (for example, GitHub). When you do need them, they're the credentials the provider issues when you register an OAuth application with them. Plain JWT validation never uses them.
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: Run an MCP server with authentication
Setting --oidc-issuer is enough to turn on token validation. Add
--oidc-audience too so ToolHive scopes acceptance to tokens meant for this
server, rather than any valid token from that issuer:
thv run \
--oidc-issuer <YOUR_OIDC_ISSUER> \
--oidc-audience <YOUR_OIDC_AUDIENCE> \
<YOUR_SERVER_NAME>
Replace the placeholders with your actual OIDC configuration. You don't need
--oidc-jwks-url unless your provider doesn't support OIDC discovery. Add
--oidc-client-id (and --oidc-client-secret) only if your provider validates
tokens by introspection instead of issuing JWTs, such as GitHub:
thv run \
--oidc-issuer <YOUR_OIDC_ISSUER> \
--oidc-audience <YOUR_OIDC_AUDIENCE> \
--oidc-client-id <YOUR_OIDC_CLIENT_ID> \
--oidc-client-secret <YOUR_OIDC_CLIENT_SECRET> \
<YOUR_SERVER_NAME>
Step 2: 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
Client support for authentication is limited at this time. While some clients support HTTP headers with SSE MCP client configurations, you should not pass JWT tokens in this way since it requires manual configuration and exposes your token in plain text.
ToolHive is working on a solution to securely handle authentication for clients. Stay tuned for updates on this feature.
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 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. This example demonstrates several policy patterns:
- Allow everyone to use the weather tool
- Restrict the admin_tool to a specific user (alice123)
- Role-based access: only users with the "premium" role can call any tool
- Attribute-based: allow the calculator tool only for add/subtract operations
Here's an example in JSON format:
{
"version": "1.0",
"type": "cedarv1",
"cedar": {
"policies": [
"permit(principal, action == Action::\"call_tool\", resource == Tool::\"weather\");",
"permit(principal == Client::\"alice123\", action == Action::\"call_tool\", resource == Tool::\"admin_tool\");",
"permit(principal, action == Action::\"call_tool\", resource) when { principal.claim_roles.contains(\"premium\") };",
"permit(principal, action == Action::\"call_tool\", resource == Tool::\"calculator\") when { resource.arg_operation == \"add\" || resource.arg_operation == \"subtract\" };"
],
"entities_json": "[]",
"group_claim_name": "",
"role_claim_name": ""
}
}
Set group_claim_name to extract group membership from a custom JWT claim, and
role_claim_name to extract role membership from a separate claim (for example,
Entra ID roles). Leave both empty to use the
default claim resolution order.
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-issuer <YOUR_OIDC_ISSUER> \
--oidc-audience <YOUR_OIDC_AUDIENCE> \
--authz-config /path/to/authz-config.json \
<YOUR_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.
Next steps
- Configure token exchange to let MCP servers authenticate to backend services
- Enable telemetry and metrics to gain observability into MCP server interactions
- Learn about the auth framework for a deeper understanding of ToolHive's authentication and authorization model
Related information
- Authentication and authorization framework for conceptual understanding
- Cedar policies for policy patterns and examples
- Authorization policy reference for the complete dictionary of entity types, actions, and attributes
- Cedar documentation for the Cedar policy language specification
Troubleshooting
Clients receive 401 Unauthorized
A 401 means token validation failed before the request reached your policy layer. Work through these checks in order:
- Check the token itself. Decode the JWT (for example, paste it into
jwt.io - never use production tokens). Verify the
issclaim matches your configured issuer URL exactly (including trailing paths like/oauth2/default), theaudclaim matches the configured audience, andexpis in the future. - Check the request. The
Authorizationheader must beAuthorization: Bearer <TOKEN>with no extra whitespace or quoting. - Check provider reachability. ToolHive first fetches the OIDC discovery
document from
${issuerUrl}/.well-known/openid-configuration, then fetches signing keys from thejwks_urilisted in that document (or from the explicitjwksUrlif you've configured one). Both URLs must be reachable. In Kubernetes, that means egress from the proxy pod; with the CLI, it means egress from your machine.
To inspect the proxy logs for the specific validation error:
# Container logs
thv logs <SERVER_NAME>
# Proxy logs (auth runs here)
thv logs <SERVER_NAME> --proxy
# Follow live
thv logs <SERVER_NAME> --proxy --follow
# Operator logs (reconciliation errors)
kubectl logs -n toolhive-system deployment/toolhive-operator
# Proxy logs for a specific MCPServer
kubectl logs -n toolhive-system \
-l app.kubernetes.io/managed-by=toolhive,app.kubernetes.io/name=<SERVER_NAME> \
-c proxy
Authenticated requests are denied (403)
A 403 means authentication succeeded but Cedar policy evaluation rejected the request. ToolHive uses default-deny: anything not explicitly permitted is denied.
- Match principal, action, and resource exactly. Cedar is case-sensitive.
Tool::"GetIssue"is not the same asTool::"get_issue". - Check policy conditions. If your policy has a
when { ... }clause, make sure the JWT actually contains the claim you reference (with theclaim_prefix). For example,principal.claim_groups.contains("writers")requires agroupsclaim on the token. - Use
hasfor tool annotations. Policies that readresource.readOnlyHint,destructiveHint,idempotentHint, oropenWorldHintmust guard the access withresource has <attr>first. Reading a missing attribute throws an evaluation error that ToolHive treats as a deny. - Confirm group claims arrive. If you use
principal in THVGroup::"<name>", decode the token and verify the configured group claim (groups,roles,cognito:groups, or whatever you set ingroup_claim_name) is present and contains the expected values.
For the full attribute and entity reference, see Authorization policy reference.