← AI Agents Security & Governance
🤖 AI Agents

RBAC for MCP Tools with Entra ID

Source: mortalapps.com
TL;DR
  • Role-Based Access Control (RBAC) for Model Context Protocol (MCP) tools leverages Microsoft Entra ID to restrict tool execution to authorized users and agents.
  • This solves the problem of unauthorized access to sensitive tools and data within AI agent systems by enforcing granular permissions.
  • In production, ignoring RBAC leads to privilege escalation, data breaches, and non-compliance, making secure tool invocation critical.
  • Implement RBAC by defining custom scopes in Entra ID, validating JWTs on the MCP server, and mapping user/agent roles to specific tool permissions.
  • It enables fine-grained control over which agents or users can invoke specific tools, ensuring operations adhere to security policies.

Why This Matters

Securing AI agent interactions with external tools is paramount for maintaining system integrity and data confidentiality. The problem of unauthorized tool invocation arises when agents, or the users operating them, can access capabilities beyond their designated permissions. This is particularly critical in enterprise environments where tools might interact with sensitive data, financial systems, or critical infrastructure. RBAC for MCP Tools with Entra ID was invented to address this by integrating robust identity and access management directly into the Model Context Protocol (MCP) tool invocation lifecycle. It ensures that every tool call is authenticated and authorized against defined roles and permissions managed centrally in Entra ID.

In production, neglecting this can lead to severe consequences. An agent with overly broad permissions, or a compromised agent, could exploit tools to exfiltrate data, perform unauthorized actions, or disrupt services, resulting in data breaches, compliance violations (e.g., GDPR, HIPAA), and significant reputational damage. When to use this approach versus a simpler API key mechanism is clear: for any system requiring dynamic, granular, and auditable access control based on user or agent identity, RBAC with Entra ID is essential. API keys offer static, all-or-nothing access, which is insufficient for complex enterprise security postures. This method provides the necessary control plane for managing tool access at scale, aligning with zero-trust principles and enabling comprehensive audit trails.

Core Concepts

Implementing RBAC for MCP tools with Entra ID requires a clear understanding of several foundational concepts:

  • Role-Based Access Control (RBAC): A method of restricting system access based on the roles of individual users within an enterprise. Roles are associated with specific permissions, and users are assigned to roles. For agent systems, 'users' can also refer to specific agents or agent types.
  • Microsoft Entra ID (formerly Azure Active Directory): Microsoft's cloud-based identity and access management service. It provides single sign-on, multi-factor authentication, and conditional access to protect against cybersecurity attacks. Entra ID acts as the Identity Provider (IdP) for issuing and validating tokens.
  • MCP Tool: An external function or service exposed to an AI agent via the Model Context Protocol. Tools perform specific actions, such as retrieving data, executing code, or interacting with APIs. Each tool can have distinct access requirements.
  • Access Token (JWT): A JSON Web Token (JWT) issued by Entra ID after successful authentication. This token contains claims, including user/agent identity, assigned roles, and requested scopes. It is sent with every MCP tool invocation for authorization.
  • Scopes (OAuth 2.1): Permissions defined for an application or API in Entra ID. When an agent or user requests an access token, they specify the scopes needed. The issued token will contain these scopes as claims if the user/agent is authorized to receive them. Scopes are granular permissions, e.g., tool.read, tool.write, tool.execute.
  • Claims: Pieces of information asserted about the subject (user or agent) of a token. Claims are key-value pairs within a JWT payload, such as sub (subject), aud (audience), iss (issuer), roles, and scp (scopes). The MCP server inspects these claims to make authorization decisions.
  • Application Registration: The process in Entra ID where an application (e.g., your MCP server) is registered to establish its identity, define its API, and configure permissions (scopes) that other applications (e.g., your agents) can request.

How It Works

RBAC for MCP tools with Entra ID follows a structured flow to ensure secure and authorized tool execution.

1. Agent Initiates Tool Invocation

An AI agent determines it needs to invoke an external tool via the Model Context Protocol. Before making the call, the agent (or the application hosting it) must acquire an OAuth 2.1 access token from Microsoft Entra ID. This token request includes the specific scopes required for the tool it intends to use, e.g., api://your-mcp-api/tool.execute.my_tool.

2. Entra ID Issues Access Token

Entra ID authenticates the agent/user and, based on their assigned roles and the application's configured permissions, issues a JWT access token. This token contains claims, including the identity of the caller, their roles, and the granted scopes. If the agent/user is not authorized for the requested scopes, Entra ID denies the token request.

3. MCP Server Receives Request

The agent sends an MCP tool_code message to the MCP server, including the acquired JWT access token in the Authorization header (Bearer token).

4. Token Validation and Claim Extraction

Upon receiving the request, the MCP server performs several critical steps:

  1. JWT Signature Validation: The server verifies the token's authenticity using Entra ID's public keys to ensure it hasn't been tampered with.
  2. Issuer and Audience Validation: It checks that the token was issued by the expected Entra ID tenant and is intended for the MCP server's application ID (audience).
  3. Expiration Check: The server ensures the token has not expired.
  4. Claim Extraction: The server extracts relevant claims, particularly the scp (scopes) and roles claims, from the token payload.

5. Authorization Decision

The MCP server's authorization logic maps the requested tool (e.g., my_tool) to a required set of permissions (scopes or roles). It then compares these required permissions against the scp and roles claims present in the validated access token. For example, if my_tool requires tool.execute.my_tool scope, the server checks if this scope is present in the token.

6. Tool Execution or Denial

  • Authorized: If the token contains the necessary scopes/roles, the MCP server proceeds to execute the requested tool. The tool's internal logic might further use the identity claims from the token for fine-grained, in-tool authorization (e.g., accessing specific user data).
  • Unauthorized: If the token lacks the required permissions, the MCP server denies the request, returning an appropriate HTTP status code (e.g., 403 Forbidden) and an error message. This prevents unauthorized access and potential misuse.

7. Response to Agent

Whether the tool was executed or denied, the MCP server sends a response back to the agent, indicating success, failure, or an authorization error.

Architecture

The conceptual architecture for RBAC-enabled MCP tools with Entra ID involves several key components interacting to enforce secure access. At the core, an AI Agent initiates requests, acting as the client. This agent communicates with a Model Context Protocol (MCP) Server, which hosts and exposes various tools. The MCP Server, in turn, relies on Microsoft Entra ID as its Identity Provider (IdP) and Authorization Server.

Execution begins when the AI Agent needs to invoke a tool. It first sends an authentication request to Entra ID to obtain an OAuth 2.1 access token, specifying the required scopes for the target tool. Entra ID authenticates the agent (or the user on whose behalf the agent acts) and, if authorized, issues a signed JWT access token. This token flows back to the AI Agent.

The AI Agent then includes this access token in the Authorization header of its MCP tool_code request, which is directed to the MCP Server. The MCP Server acts as a Resource Server. It intercepts the incoming request, extracts the JWT, and performs validation by communicating with Entra ID's public key endpoints to verify the token's signature, issuer, audience, and expiration. After validation, the MCP Server extracts the scp (scopes) and roles claims from the token.

Based on these claims, the MCP Server's internal authorization module determines if the agent possesses the necessary permissions to execute the specific requested tool. If authorized, the MCP Server invokes the underlying Tool Service, which performs the actual business logic. The Tool Service returns its result to the MCP Server, which then forwards the final response back to the AI Agent. If authorization fails at any point, the MCP Server returns a 403 Forbidden error to the agent, preventing tool execution.

1. Entra ID Application Registration and API Permissions

To enable RBAC, your MCP server must be registered as an application in Microsoft Entra ID. This registration defines the server's identity and exposes its API. Navigate to the Entra ID admin center, create a new 'App registration', and configure it as a 'Web' application. Crucially, you must define the API by adding custom scopes. Each scope represents a granular permission for a specific tool or a category of tools. For instance, tool.execute.my_data_tool or tool.read.financial_report. These scopes are published by your MCP server application and can then be requested by client applications (your AI agents).

2. Defining Custom Scopes for Tools

Within your MCP server's App Registration in Entra ID, go to 'Expose an API' and define your custom scopes. Each scope needs a name (e.g., tool.execute.data_query), an admin consent display name, and an admin consent description. It's good practice to make scopes descriptive and specific. For example, a data_query tool might expose tool.read.data_query and tool.write.data_query scopes. This allows for different levels of access. When an agent requests a token, it will specify which of these scopes it needs. Entra ID will only grant scopes that the requesting agent's application (also registered in Entra ID) has been granted permission to request, and for which the user/agent has been assigned roles that permit those scopes.

3. Configuring the MCP Server for Token Validation

Your MCP server, typically a Python web application (e.g., Flask, FastAPI), needs to be configured to validate incoming JWTs. This involves using a library like python-jose or PyJWT with msal for token validation. The server must fetch Entra ID's OpenID Connect metadata endpoint (e.g., https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration) to retrieve the public keys (JWKS) and other configuration details. These keys are used to verify the signature of the incoming JWT. Key validation steps include:

  • Signature Verification: Ensure the token was signed by Entra ID.
  • Issuer Validation: Confirm the iss claim matches your Entra ID tenant.
  • Audience Validation: Verify the aud claim matches your MCP server's application ID.
  • Expiration Check: Reject expired tokens.
  • Nonce/Replay Protection: For certain flows, though less critical for Bearer tokens in API calls, consider if applicable.
# Example of token validation setup (simplified)
import jwt
import requests
from cachetools import cached, TTLCache

# Environment variables for Entra ID configuration
TENANT_ID = os.environ.get("AZURE_TENANT_ID")
CLIENT_ID = os.environ.get("AZURE_CLIENT_ID") # MCP Server's App ID

# Cache for JWKS to reduce network calls
JWKS_CACHE = TTLCache(maxsize=1, ttl=3600) # Cache for 1 hour

@cached(JWKS_CACHE)
def get_jwks(tenant_id):
    openid_config_url = f"https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration"
    config = requests.get(openid_config_url).json()
    jwks_uri = config["jwks_uri"]
    return requests.get(jwks_uri).json()

def validate_jwt(token: str):
    try:
        jwks = get_jwks(TENANT_ID)
        header = jwt.get_unverified_header(token)
        key = next(k for k in jwks["keys"] if k["kid"] == header["kid"])

        # Decode and validate the token
        decoded_token = jwt.decode(
            token,
            key=key,
            algorithms=[header["alg"]],
            audience=CLIENT_ID,
            issuer=f"https://login.microsoftonline.com/{TENANT_ID}/",
            options={
                "verify_signature": True,
                "verify_exp": True,
                "verify_nbf": True,
                "verify_iat": True,
                "verify_aud": True,
                "verify_iss": True,
            }
        )
        return decoded_token
    except jwt.ExpiredSignatureError:
        raise ValueError("Token has expired")
    except jwt.InvalidTokenError as e:
        raise ValueError(f"Invalid token: {e}")
    except Exception as e:
        raise ValueError(f"Token validation error: {e}")

4. Implementing Tool-Level Authorization Logic

Once the token is validated and claims are extracted, the MCP server's tool dispatcher must implement authorization logic. For each tool, define the required scopes. When a tool invocation request arrives, retrieve the scp claim from the decoded JWT. This claim is a space-separated string of granted scopes. Check if the required scope for the requested tool exists within this string. If not, deny access.

# Example of tool authorization logic
def authorize_tool_invocation(decoded_token: dict, tool_name: str):
    required_scope = f"tool.execute.{tool_name}"
    granted_scopes = decoded_token.get("scp", "").split()

    if required_scope not in granted_scopes:
        raise PermissionError(f"Missing required scope: {required_scope}")
    
    # Additional role-based checks can be performed here
    # user_roles = decoded_token.get("roles", [])
    # if "Admin" not in user_roles and "Editor" not in user_roles:
    #     raise PermissionError("User does not have required role")

    print(f"Authorization successful for tool '{tool_name}' with scopes: {granted_scopes}")
    return True

5. Agent-Side Scope Request

On the agent side, when configuring the client to acquire an access token, ensure it requests the correct scopes. The agent's application registration in Entra ID must have permissions to request these scopes from your MCP server's API. This is typically configured in the 'API permissions' section of the agent's app registration, where you add permissions to your MCP server's exposed API. The agent then uses an OAuth 2.1 client library (e.g., msal-python) to obtain a token, specifying the scope parameter with the required API URI and scope names.

# Example of agent requesting token with scopes (simplified)
import msal

# Environment variables for Entra ID configuration
AGENT_CLIENT_ID = os.environ.get("AZURE_AGENT_CLIENT_ID")
AGENT_CLIENT_SECRET = os.environ.get("AZURE_AGENT_CLIENT_SECRET")
AGENT_TENANT_ID = os.environ.get("AZURE_TENANT_ID")

AUTHORITY = f"https://login.microsoftonline.com/{AGENT_TENANT_ID}"
MCP_API_SCOPE = os.environ.get("MCP_API_SCOPE", "api://your-mcp-api-id/.default") # .default requests all configured scopes

app = msal.ConfidentialClientApplication(
    AGENT_CLIENT_ID,
    authority=AUTHORITY,
    client_credential=AGENT_CLIENT_SECRET
)

def get_agent_access_token(scopes: list[str]):
    try:
        result = app.acquire_token_for_client(scopes=scopes)
        if "access_token" in result:
            return result["access_token"]
        else:
            raise ValueError(f"Failed to acquire token: {result.get('error_description', 'Unknown error')}")
    except Exception as e:
        raise RuntimeError(f"Error acquiring token: {e}")

# Example usage:
# token = get_agent_access_token(scopes=[MCP_API_SCOPE, "api://your-mcp-api-id/tool.execute.my_tool"])
# print(f"Acquired token: {token[:30]}...")

Code Example

This Python Flask example demonstrates an MCP server endpoint that validates an Entra ID JWT and authorizes a tool invocation based on required scopes. It simulates a simple 'data_query' tool.
Python
import os
import jwt
import requests
from flask import Flask, request, jsonify
from functools import wraps
from cachetools import cached, TTLCache
import logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)

# Environment variables for Entra ID configuration
TENANT_ID = os.environ.get("AZURE_TENANT_ID")
CLIENT_ID = os.environ.get("AZURE_CLIENT_ID") # MCP Server's App ID

if not all([TENANT_ID, CLIENT_ID]):
    logging.error("Missing AZURE_TENANT_ID or AZURE_CLIENT_ID environment variables.")
    exit(1)

# Cache for JWKS to reduce network calls
JWKS_CACHE = TTLCache(maxsize=1, ttl=3600) # Cache for 1 hour

@cached(JWKS_CACHE)
def get_jwks(tenant_id):
    try:
        openid_config_url = f"https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration"
        config = requests.get(openid_config_url, timeout=5).json()
        jwks_uri = config["jwks_uri"]
        return requests.get(jwks_uri, timeout=5).json()
    except requests.exceptions.RequestException as e:
        logging.error(f"Failed to fetch JWKS: {e}")
        raise ConnectionError("Could not retrieve JWKS from Entra ID.")

def validate_jwt(token: str):
    try:
        jwks = get_jwks(TENANT_ID)
        header = jwt.get_unverified_header(token)
        key = next((k for k in jwks["keys"] if k["kid"] == header["kid"]), None)
        if not key:
            raise ValueError("No matching JWK found for token KID.")

        decoded_token = jwt.decode(
            token,
            key=key,
            algorithms=[header["alg"]],
            audience=CLIENT_ID,
            issuer=f"https://login.microsoftonline.com/{TENANT_ID}/",
            options={
                "verify_signature": True,
                "verify_exp": True,
                "verify_nbf": True,
                "verify_iat": True,
                "verify_aud": True,
                "verify_iss": True,
            }
        )
        return decoded_token
    except jwt.ExpiredSignatureError:
        logging.warning("JWT expired.")
        raise ValueError("Token has expired")
    except jwt.InvalidTokenError as e:
        logging.error(f"Invalid JWT: {e}")
        raise ValueError(f"Invalid token: {e}")
    except (ConnectionError, ValueError) as e:
        logging.error(f"Token validation setup error: {e}")
        raise
    except Exception as e:
        logging.critical(f"Unexpected error during JWT validation: {e}")
        raise ValueError("Internal server error during token validation.")

def requires_scope(required_scope: str):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            auth_header = request.headers.get('Authorization')
            if not auth_header or not auth_header.startswith('Bearer '):
                return jsonify({"error": "Authorization token missing or malformed"}), 401
            
            token = auth_header.split(' ')[1]
            try:
                decoded_token = validate_jwt(token)
                granted_scopes = decoded_token.get("scp", "").split()
                if required_scope not in granted_scopes:
                    logging.warning(f"Access denied: Missing scope '{required_scope}'. Granted: {granted_scopes}")
                    return jsonify({"error": f"Forbidden: Missing required scope '{required_scope}'"}), 403
                request.decoded_token = decoded_token # Attach token for downstream use
            except ValueError as e:
                logging.error(f"Authentication failed: {e}")
                return jsonify({"error": str(e)}), 401
            except ConnectionError as e:
                logging.error(f"Authentication service unavailable: {e}")
                return jsonify({"error": "Authentication service unavailable, please try again later."}), 503
            
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/mcp/tool/data_query', methods=['POST'])
@requires_scope('tool.execute.data_query')
def data_query_tool():
    # In a real scenario, the tool invocation would happen here.
    # The decoded_token could be used for further granular authorization within the tool.
    user_id = request.decoded_token.get('oid') # Example: Object ID of the user/agent
    logging.info(f"Executing data_query tool for user/agent: {user_id}")
    try:
        # Simulate tool execution
        request_data = request.get_json()
        query = request_data.get('query', 'default_query')
        result = {"status": "success", "data": f"Results for '{query}' by {user_id}"}
        return jsonify(result), 200
    except Exception as e:
        logging.error(f"Error executing data_query tool: {e}")
        return jsonify({"error": "Tool execution failed"}), 500

@app.route('/mcp/tool/admin_action', methods=['POST'])
@requires_scope('tool.execute.admin_action')
def admin_action_tool():
    user_id = request.decoded_token.get('oid')
    logging.info(f"Executing admin_action tool for user/agent: {user_id}")
    try:
        result = {"status": "success", "message": f"Admin action completed by {user_id}"}
        return jsonify(result), 200
    except Exception as e:
        logging.error(f"Error executing admin_action tool: {e}")
        return jsonify({"error": "Tool execution failed"}), 500

if __name__ == '__main__':
    # For local development, set these in your environment or a .env file
    # export AZURE_TENANT_ID="<your-tenant-id>"
    # export AZURE_CLIENT_ID="<your-mcp-server-app-id>"
    app.run(debug=True, port=5000)
Expected Output
When a request is sent to `/mcp/tool/data_query` with a valid Entra ID JWT in the `Authorization` header containing the `tool.execute.data_query` scope, the server will respond with `{"status": "success", "data": "Results for '...' by ..."}` and HTTP 200. If the token is missing, invalid, expired, or lacks the required scope, an appropriate error (e.g., 401, 403) and error message will be returned. Similarly for `/mcp/tool/admin_action` requiring `tool.execute.admin_action`.

Key Takeaways

RBAC for MCP tools with Entra ID is essential for enforcing granular access control in AI agent systems.
Entra ID acts as the central identity provider, issuing JWTs with scopes and roles that dictate tool access.
MCP servers must perform robust JWT validation, including signature, issuer, audience, and expiration checks.
Granular scopes defined in Entra ID application registrations enable fine-grained permission management for individual tools.
Caching Entra ID's JWKS significantly reduces latency and improves the performance of token validation on the MCP server.
Comprehensive logging of authorization decisions is critical for auditing, compliance, and security incident response.
Proper RBAC implementation mitigates risks like privilege escalation and unauthorized data access in production agent systems.

Frequently Asked Questions

What is the primary benefit of using Entra ID for RBAC with MCP tools? +
The primary benefit is centralized, enterprise-grade identity and access management, enabling granular control, auditability, and integration with existing corporate security policies for AI agent tool access.
How does an agent acquire an access token from Entra ID? +
An agent (or its hosting application) uses an OAuth 2.1 client library (e.g., MSAL) to request a token from Entra ID, specifying the required scopes for the tools it intends to use.
What happens if an agent's token does not have the required scope for a tool? +
The MCP server's authorization logic will deny the request, typically returning an HTTP 403 Forbidden status, preventing the unauthorized tool from being executed.
When should I avoid using Entra ID for MCP tool RBAC? +
Avoid it for simple, isolated agent systems without enterprise security requirements, where a basic API key might suffice, or if your organization does not use Microsoft Entra ID as its primary IdP.
What are the limitations of this RBAC approach? +
Limitations include the overhead of token validation, reliance on Entra ID's availability, and the complexity of managing numerous granular scopes for very large toolsets. It also requires careful setup of application registrations.
How does this work with other identity providers besides Entra ID? +
The core principles of JWT validation and scope checking remain similar. You would configure your MCP server to validate tokens issued by your chosen IdP (e.g., Okta, Auth0) and define scopes within that provider's system.
What happens when Entra ID is temporarily unavailable? +
If Entra ID is unavailable, new token acquisition will fail. Token validation on the MCP server might also fail if JWKS cannot be refreshed, potentially leading to service disruption. Caching JWKS helps mitigate this for existing tokens.
Can I use Entra ID roles instead of scopes for authorization? +
Yes, Entra ID roles can be included as claims in the JWT. The MCP server can then check for specific roles (e.g., 'ToolAdmin', 'DataAnalyst') in addition to or instead of scopes, depending on your authorization model.
How do I ensure that my agent's application registration in Entra ID is secure? +
Follow best practices: use strong client secrets, rotate them regularly, apply conditional access policies, and grant only the minimum necessary API permissions to the agent's registration.