Secure-by-Default Authorization for MCP Servers powered by ToolHive

As more teams start deploying MCP servers to power tool-calling agents, one question comes up fast: how do you control who can call what? It’s not just about verifying identity, it’s about enforcing the right permissions, without bloating every server with bespoke auth logic. ToolHive was built to solve exactly this problem. It separates authentication from authorization, integrates cleanly with existing identity providers, and uses Amazon’s Cedar policy language to define clear, auditable access rules. The result is a simple but powerful way to lock down tool access without embedding auth logic into every server you run. Authentication vs Authorization: Separate Concerns In my previous blog post, I outlined a fundamental design choice in ToolHive: the strong separation of authentication (authN) and authorization (authZ). Authentication verifies identity (proving who you are), while authorization determines what that identity is allowed to do. ToolHive treats these as two distinct steps – first authenticate the caller, then check what they can access. In practice, ToolHive uses OpenID Connect (OIDC) to handle authentication (e.g., verifying a user's JWT from a trusted identity provider) and only afterwards applies its own permission rules for MCP actions. By explicitly separating authN and authZ, ToolHive can rely on well-proven identity systems and avoid conflating identity with access control, leading to a cleaner, more secure design. This separation matters because it lets ToolHive plug into existing single sign-on/IdP solutions for identifying users, while maintaining a clear, flexible policy model for what those users can do within MCP servers. ToolHive’s Authorization Framework Overview A few weeks ago, I had lunch with Lucas Käldström and had a good chat about authorization primitives and the work he’s been doing with Amazon’s Cedar policy language in relation to Kubernetes. Let’s just say that chat was exciting enough for me to try it out, and an authorization framework for ToolHive was born! As mentioned before, ToolHive’s authorization system is built on Amazon’s Cedar policy language and is designed as a layer on top of the base MCP server. This authorization layer is tightly integrated with ToolHive’s existing JWT-based authentication middleware. In a typical deployment, the request flow is: a JWT validator middleware first verifies the user's identity token, and then the Cedar authorization middleware runs next. This means ToolHive acts as a gateway in front of the MCP server, handling auth on the server’s behalf. The MCP server itself doesn’t need to implement OAuth or check tokens – ToolHive has it covered. How Authorization Works in ToolHive Once an MCP server is launched with ToolHive’s authZ enabled (by passing an --authz-config file), every client request goes through an authorization check before reaching the server logic. At a high level, the process is as follows: Authenticate – ToolHive’s JWT middleware verifies the client’s token (e.g., an OIDC ID token or other JWT) and, if valid, attaches the user’s identity and claims to the request context. This tells ToolHive who is making the request (the principal). Extract Request Info – The authorization middleware then examines the incoming MCP request. It determines what operation is being attempted and on what resource. For example, it might see a JSON-RPC call for the method tools/call with tool name "weather". From this, ToolHive derives the Action (like "call_tool") and the Resource (like the "weather" tool) being requested. Policy Evaluation – ToolHive invokes the Cedar authorizer with the gathered context (the principal identity, the action type, and the resource target). The Cedar engine checks the loaded policies to decide if this principal is permitted to perform that action on that resource. This evaluation considers any relevant policy rules defined in the config file, including conditions based on user attributes or tool attributes (more on this below). Allow or Deny – If the policies permit the action, the request is forwarded to the MCP server’s normal processing. If not (no policy allowed it, or an explicit deny policy matches), ToolHive stops the request and returns an HTTP 403 Forbidden error (or an error in the SSE/JSON-RPC response) to the client. The MCP server never sees unauthorized requests. In essence, ToolHive acts as a policy enforcement point in front of the MCP server. Unauthorized tool invocations, prompt fetches, etc., are blocked at the gate. This design greatly reduces the risk of a malicious or unauthorized request ever hitting the actual model server logic. Policy Model: Cedar in ToolHive The heart of ToolHive’s authorization is the Cedar policy language. Cedar is a flexible, expressive, and formally verified language for access control, supporting both role-based and attribute-based rules. ToolHive leverages Cedar to define who can do what in terms of M

Apr 16, 2025 - 07:10
 0
Secure-by-Default Authorization for MCP Servers powered by ToolHive

As more teams start deploying MCP servers to power tool-calling agents, one question comes up fast: how do you control who can call what? It’s not just about verifying identity, it’s about enforcing the right permissions, without bloating every server with bespoke auth logic. ToolHive was built to solve exactly this problem. It separates authentication from authorization, integrates cleanly with existing identity providers, and uses Amazon’s Cedar policy language to define clear, auditable access rules. The result is a simple but powerful way to lock down tool access without embedding auth logic into every server you run.

Authentication vs Authorization: Separate Concerns

In my previous blog post, I outlined a fundamental design choice in ToolHive: the strong separation of authentication (authN) and authorization (authZ). Authentication verifies identity (proving who you are), while authorization determines what that identity is allowed to do. ToolHive treats these as two distinct steps – first authenticate the caller, then check what they can access. In practice, ToolHive uses OpenID Connect (OIDC) to handle authentication (e.g., verifying a user's JWT from a trusted identity provider) and only afterwards applies its own permission rules for MCP actions. By explicitly separating authN and authZ, ToolHive can rely on well-proven identity systems and avoid conflating identity with access control, leading to a cleaner, more secure design. This separation matters because it lets ToolHive plug into existing single sign-on/IdP solutions for identifying users, while maintaining a clear, flexible policy model for what those users can do within MCP servers.

ToolHive’s Authorization Framework Overview

A few weeks ago, I had lunch with Lucas Käldström and had a good chat about authorization primitives and the work he’s been doing with Amazon’s Cedar policy language in relation to Kubernetes. Let’s just say that chat was exciting enough for me to try it out, and an authorization framework for ToolHive was born!

As mentioned before, ToolHive’s authorization system is built on Amazon’s Cedar policy language and is designed as a layer on top of the base MCP server. This authorization layer is tightly integrated with ToolHive’s existing JWT-based authentication middleware. In a typical deployment, the request flow is: a JWT validator middleware first verifies the user's identity token, and then the Cedar authorization middleware runs next. This means ToolHive acts as a gateway in front of the MCP server, handling auth on the server’s behalf. The MCP server itself doesn’t need to implement OAuth or check tokens – ToolHive has it covered.

How Authorization Works in ToolHive

Once an MCP server is launched with ToolHive’s authZ enabled (by passing an --authz-config file), every client request goes through an authorization check before reaching the server logic. At a high level, the process is as follows:

  1. Authenticate – ToolHive’s JWT middleware verifies the client’s token (e.g., an OIDC ID token or other JWT) and, if valid, attaches the user’s identity and claims to the request context. This tells ToolHive who is making the request (the principal).

  2. Extract Request Info – The authorization middleware then examines the incoming MCP request. It determines what operation is being attempted and on what resource. For example, it might see a JSON-RPC call for the method tools/call with tool name "weather". From this, ToolHive derives the Action (like "call_tool") and the Resource (like the "weather" tool) being requested.

  3. Policy Evaluation – ToolHive invokes the Cedar authorizer with the gathered context (the principal identity, the action type, and the resource target). The Cedar engine checks the loaded policies to decide if this principal is permitted to perform that action on that resource. This evaluation considers any relevant policy rules defined in the config file, including conditions based on user attributes or tool attributes (more on this below).

  4. Allow or Deny – If the policies permit the action, the request is forwarded to the MCP server’s normal processing. If not (no policy allowed it, or an explicit deny policy matches), ToolHive stops the request and returns an HTTP 403 Forbidden error (or an error in the SSE/JSON-RPC response) to the client. The MCP server never sees unauthorized requests.

In essence, ToolHive acts as a policy enforcement point in front of the MCP server. Unauthorized tool invocations, prompt fetches, etc., are blocked at the gate. This design greatly reduces the risk of a malicious or unauthorized request ever hitting the actual model server logic.

Policy Model: Cedar in ToolHive

The heart of ToolHive’s authorization is the Cedar policy language. Cedar is a flexible, expressive, and formally verified language for access control, supporting both role-based and attribute-based rules. ToolHive leverages Cedar to define who can do what in terms of MCP operations.

Principals, Actions, Resources: In Cedar (and ToolHive’s usage), every access policy involves these three entities. ToolHive defines them in the context of MCP as follows:

  • Principal – the client or user making the request. In ToolHive, principals are identified by the OIDC/JWT subject. For example, a user with ID "user123" would be represented as Client::user123 in policies. (ToolHive uses a Client entity type for principals.)

  • Action – the operation being performed. ToolHive categorizes MCP operations like calling a tool, reading a resource, listing prompts, etc. Examples include Action::"call_tool", Action::"get_prompt", Action::"list_tools", and so on. The authorization middleware translates each incoming request to the appropriate action tag.

  • Resource – the target object of the action. This could be a specific tool name, a prompt name, or a resource identifier. In policies, resources are namespaced by type, like Tool::"weather" for the "weather" tool, Prompt::"greeting" for the "greeting" prompt, Resource::"data" for a resource called "data", or even a category like FeatureType::"tool" to represent “any tool” in a list operation.

Using these, you can write fine-grained rules. A Cedar policy generally looks like:

permit(principal, action, resource) when {  };

This means "allow this principal performing this action on this resource if certain conditions hold." (Cedar also supports forbid rules to explicitly disallow actions.) In ToolHive’s config, policies are defined as strings. For example, the ToolHive docs show a simple policy set that permits any user to call the "weather" tool, get the "greeting" prompt, and read the "data" resource:

{
  "version": "1.0",
  "type": "cedarv1",
  "cedar": {
    "policies": [
      "permit(principal, action == Action::\\\"call_tool\\\", resource == Tool::\\\"weather\\\");",
      "permit(principal, action == Action::\\\"get_prompt\\\", resource == Prompt::\\\"greeting\\\");",
      "permit(principal, action == Action::\\\"read_resource\\\", resource == Resource::\\\"data\\\");"
    ],
    "entities_json": "[]"
  }
}

Each string in the policies array is a Cedar rule. For instance, the first rule above says any principal can call_tool on the weather tool. If a request comes in to invoke the weather tool, this policy would allow it (assuming no conflicting denies).

Attributes and Conditions: One of the strengths of Cedar (and ToolHive’s approach) is the ability to use attributes in policies for richer conditions – this is essentially attribute-based access control (ABAC). ToolHive’s middleware automatically makes certain attributes available to policies:

  • JWT Claims – All standard claims from the authenticated user's JWT are added as attributes on the principal, prefixed with claim_. For example, if a user's token has a claim roles: ["admin"], then in policy you can refer to principal.claim_roles to check that. A policy might require principal.claim_roles.contains("admin") in its condition to restrict an action to admins only. Similarly, principal.claim_name could be their name, etc., allowing policies to target specific users or user properties.

  • Tool Arguments – If the MCP action involves calling a tool with arguments (for example, a calculator tool with an operation argument "add" vs "subtract"), those arguments are exposed as attributes on the resource, prefixed with arg_ . This means policies can even inspect the parameters of a tool invocation. For instance, you could allow use of a tool only for certain argument values. The ToolHive docs show a policy that permits calling the "calculator" tool only if the operation is "add" or "subtract", by checking resource.arg_operation in the policy condition.

For convenience, these attributes are also exposed in the authorizer’s context. That is, you can access them via the context key.

Additionally, administrators can define custom resource attributes via the entities_json in the config. This JSON string can enumerate entities (like specific tools) with custom attributes (like an owner or sensitivity label). At runtime, the Cedar authorizer will include those entity definitions. For example, you could mark the "weather" tool entity with an "owner": "user123" attribute, and then write a policy that permits access only if resource.owner == principal.claim_sub . This would mean only the owner of a tool can use it, effectively implementing per-tool ownership controls.

Policy Decision Logic: ToolHive’s policy evaluation follows a secure-by-default logic: if nothing explicitly allows a request, it’s denied. In fact, Cedar policies can be either permit or forbid rules, and ToolHive applies them with deny precedence – any matching forbid rule will override permits. During evaluation: (1) if any forbid policy matches, the request is denied; (2) otherwise, if any permit policy matches, the request is allowed; (3) if no policies match, the request is denied by default. This default-deny approach ensures that in the absence of a rule, access is not allowed (principle of least privilege). As a ToolHive admin, you therefore list out what is permitted (and optionally what is explicitly forbidden), and everything else is automatically blocked.

Example: Writing and Using Policies

Let’s illustrate a few typical authorization rules you might implement in ToolHive (using Cedar syntax):

  • Allow everyone to use a specific tool: For instance, to open up a weather info tool to all users, you’d add a policy:
permit(principal, action == Action::"call_tool", resource == Tool::"weather");

This means any authenticated principal can call the weather tool. (You might pair this with other policies to keep other tools locked down.)

  • Restrict a tool to certain users: Suppose only the user with ID "alice123" should use an internal admin tool. You could write:
permit(principal == Client::"alice123", action == Action::"call_tool", resource == Tool::"admin_tool");

This permits only that specific user (principal) to call the admin_tool. No one else’s principal ID will match, so others will be denied (by default).

  • Role-based rule: If your JWT tokens include roles, you can use them. For example:
permit(principal, action == Action::"call_tool", resource) 
  when { principal.claim_roles.contains("premium") };

This would allow any user with the role "premium" to call any tool (maybe to grant paying users access to certain features), while denying calls from users without that role.

  • Tool argument condition: As mentioned, you can gate based on tool parameters. For a calculator tool that has an argument "operation", you might write:
permit(principal, action == Action::"call_tool", resource == Tool::"calculator") 
  when { resource.arg_operation == "add" || resource.arg_operation == "subtract" };

This ensures the calculator tool can only be used for addition or subtraction, and would block other operations like "multiply" if the tool supports them.

These examples show the expressive power of ToolHive’s authorization policies. This approach makes it easy for developers and operators to reason about who can do what on their MCP servers without modifying the server code itself.

ToolHive vs. MCP Specification’s Authorization Guidance

It’s worth noting how ToolHive’s approach compares to the official Model Context Protocol (MCP) specification’s guidance on authorization. The MCP spec (as of the 2025-03-26 revision) outlines an OAuth 2.1-based authorization model for HTTP transports. In other words, an MCP server could implement a standard OAuth flow: clients obtain an access token (via some OAuth authorization server), present that token to the MCP server, and the server validates it and checks scopes/permissions. The spec makes this kind of transport-level authorization optional but recommended for HTTP-based MCP servers. It even describes a scenario where an MCP server might delegate to an external OAuth provider (like redirecting the user to sign in with a third-party, then exchanging tokens).

ToolHive takes a different path. Rather than turning every MCP server into an OAuth resource server (or running a full OAuth authorization dance for each tool), ToolHive centralizes auth in its proxy layer. It uses OIDC for authentication (OAuth2 under the hood, but handled by an external IdP) and then its Cedar policy engine for fine-grained authorization. This means as a developer or admin, you don't have to implement OAuth flows in each MCP server or manage scope definitions for each tool. Instead, you configure ToolHive with the identity provider of your choice (Google, GitHub, corporate SSO, etc. for OIDC login) and write straightforward policies to declare what’s allowed. The heavy lifting of token verification is done once in ToolHive’s front door, and the permission logic is under your control via Cedar policies.

In summary, the MCP spec focuses on a standardized mechanism (OAuth 2.1) to allow clients to access restricted servers – great for interoperability, but it can be complex to implement directly. ToolHive’s approach is built for convenience and control: it uses standard OIDC to know who the user is, but then gives you a powerful, code-driven way to define authorization rules without a separate OAuth service for each server. This approach trades the spec’s one-size-fits-all OAuth solution for a more flexible policy model that is easily tailored to the needs of different tools and deployments.

Security and Usability

ToolHive’s approach to authorization within MCP emphasizes secure defaults, integration with existing identity systems, and a developer-friendly policy model. By separating authN and authZ, using a powerful policy language, and acting as a smart proxy, ToolHive achieves a balance of security and usability. Developers can spin up MCP servers with confidence that only the right people (or services) can invoke the right tools, all configured through a clear and versionable policy file. This makes running MCP servers in production (especially in enterprise environments) much more manageable and auditable, without sacrificing the flexibility that makes the Model Context Protocol so powerful.