MCP OAuth 2.1 Authorization with Entra ID
Source: mortalapps.com- Standardizes secure tool invocation by implementing OAuth 2.1 authorization code grants with PKCE S256 for Model Context Protocol (MCP) servers.
- Solves the 'God Mode' problem where agents operate with excessive, static privileges by enforcing user-delegated, short-lived access tokens.
- Critical for enterprise production environments requiring Zero Trust architecture and auditability at the tool-call boundary.
- Enables seamless integration with Microsoft Entra ID for centralized identity management and Conditional Access policy enforcement.
Why This Matters
Securing remote tool calls in agentic systems requires moving beyond static API keys toward a robust identity-centric model. Implementing mcp oauth 2.1 entra id integration provides the necessary framework for delegated authorization, ensuring that an AI agent only performs actions the end-user is explicitly permitted to execute. This approach was necessitated by the rise of 'agentic' workflows where autonomous systems interact with sensitive enterprise data across distributed MCP servers. Without a standardized authorization layer, organizations face significant production risks, including privilege escalation, where a compromised agent or a prompt injection attack could exploit over-privileged tool connections to exfiltrate data or perform unauthorized mutations. In an enterprise context, ignoring this leads to a violation of the Principle of Least Privilege (PoLP) and fails compliance audits under frameworks like SOC2 or HIPAA. While simpler methods like basic auth or static tokens might suffice for local development, they break at scale due to the lack of centralized revocation, rotation, and fine-grained scoping. Using OAuth 2.1 with Entra ID allows architects to leverage existing identity investments, such as Multi-Factor Authentication (MFA) and device compliance checks, extending these protections directly to the point of tool execution. This blueprint provides the architectural rationale for selecting PKCE (Proof Key for Code Exchange) to secure public clients - such as local agent runtimes - where client secrets cannot be safely stored, ensuring a secure handshake even in non-confidential environments.
Core Concepts
Implementing OAuth 2.1 within MCP requires a precise understanding of several identity primitives:
- Authorization Code Grant with PKCE: A flow designed for public clients that replaces the requirement for a client secret with a dynamically generated cryptographic challenge (Code Verifier and Code Challenge).
- S256 Code Challenge: The SHA-256 hash of the code verifier, used to prevent authorization code injection attacks.
- Dynamic Client Registration (DCR): A mechanism allowing MCP servers to provide metadata that helps the client register itself or locate the correct identity provider (IdP) endpoint dynamically.
- Entra ID Service Principals: The identity representation of the MCP server within the Azure/Microsoft 365 tenant, defining the scopes and permissions available to the agent.
- Scopes (Permissions): Fine-grained strings (e.g.,
Files.Read,Tools.Execute) that define exactly what the agent is authorized to do on behalf of the user. - Bearer Tokens (JWT): Short-lived JSON Web Tokens passed in the HTTP Authorization header to prove the agent's right to access a specific MCP tool.
How It Works
The MCP OAuth 2.1 lifecycle follows a structured sequence to ensure that every tool invocation is backed by a valid, user-consented token.
1. Discovery and Metadata Exchange
When an MCP Client (the agent runtime) connects to an MCP Server, it first queries the server's capabilities. If the server requires authorization, it returns an auth_metadata object containing the Entra ID authorization endpoint, token endpoint, and required scopes. This phase ensures the client knows exactly where to direct the user for authentication.
2. PKCE Challenge Generation
The client generates a high-entropy cryptographic random string called the code_verifier. It then derives the code_challenge by applying a SHA-256 hash to the verifier and base64-url encoding the result. This pair is essential for securing the flow without a pre-shared secret.
3. User Authorization
The client opens a browser or system-level redirect to the Entra ID authorization endpoint, passing the code_challenge and the requested scopes. The user authenticates via Entra ID (potentially involving MFA). Upon success, Entra ID redirects back to the client with a temporary authorization_code.
4. Token Exchange
The client sends the authorization_code and the original code_verifier to the Entra ID token endpoint. Entra ID verifies that the hash of the verifier matches the challenge sent in step 3. If valid, it issues an access_token (JWT) and a refresh_token.
5. Tool Invocation with Bearer Token
For every subsequent MCP tool call, the client includes the access_token in the request headers. The MCP Server validates the JWT's signature against Entra ID's public keys and checks the scp (scopes) and exp (expiration) claims. If validation fails or the token is expired, the server returns a 401 Unauthorized error, triggering a refresh flow or re-authentication.
Architecture
The architecture consists of four primary components interacting in a secure loop. The MCP Client (e.g., a desktop agent or cloud orchestrator) acts as the Orchestrator, managing the PKCE state and token storage. The MCP Server acts as the Resource Server, hosting the tools but delegating identity verification to the IdP. Microsoft Entra ID serves as the Identity Provider (IdP), maintaining the user directory, application registrations, and issuing signed JWTs. Finally, the Tool Provider is the backend logic or API that the MCP server wraps. Execution starts at the MCP Client when a tool call is needed. The client checks its local cache for a valid token. If missing, it initiates the OAuth flow with Entra ID. Once the token is acquired, it flows through the MCP Client to the MCP Server via a secure transport (like Streamable HTTP). The MCP Server validates the token before allowing the request to reach the Tool Provider. The flow ends when the Tool Provider's response is returned through the MCP Server back to the client.
PKCE S256 Implementation Details
In the context of MCP, the client is often a local application or a transient process where a client_secret cannot be kept confidential. OAuth 2.1 mandates PKCE for these scenarios. The code_verifier must be a cryptographically random string between 43 and 128 characters. The transformation to code_challenge using S256 is non-negotiable in enterprise environments to prevent 'Authorization Code Interception' attacks. If a malicious actor intercepts the authorization code, they cannot exchange it for a token because they lack the original code_verifier that matches the hashed challenge stored at Entra ID.
Entra ID App Registration Strategy
For MCP servers, two registration patterns are common. In a Single-Tenant setup, the MCP server and the users are in the same Azure tenant, simplifying trust but limiting the server to one organization. In a Multi-Tenant setup, the MCP server is registered in a 'provider' tenant, and users from any tenant can consent to the application. This requires the MCP server to handle the common or organizations endpoints and validate the tid (tenant ID) claim in the JWT to ensure the user belongs to an authorized customer organization.
Scope Mapping and Tool-Level Granularity
One of the most powerful features of using Entra ID with MCP is the ability to map specific tools to specific OAuth scopes. For example, an MCP server might offer get_email and delete_email. The server can require the Mail.Read scope for the former and Mail.ReadWrite for the latter. The MCP client must inspect the server's tool metadata to determine which scopes to request during the initial authorization. This prevents 'scope creep' where an agent is granted broad permissions when it only needs to perform a single, narrow task.
Handling Token Expiration and Refresh
Access tokens from Entra ID typically expire in 60-90 minutes. To maintain a seamless agent experience, the MCP client must implement a background refresh logic using the refresh_token. The client should attempt to refresh the token when it is within 5-10 minutes of expiration. If the refresh token is also expired or has been revoked (e.g., due to a password change or a Conditional Access policy trigger), the client must be prepared to pause the agent's execution and prompt the user for a fresh interactive login. This 'interruption' is a critical security feature, not a bug, as it ensures continuous compliance with the IdP's security posture.
Client Credentials Grant (Machine-to-Machine)
For server-to-server or service-account MCP connections (no human user), OAuth 2.1 also supports the Client Credentials grant:
- The MCP client sends
grant_type=client_credentialswith itsclient_idandclient_secretto the token endpoint. - The authorization server returns an
access_token(no refresh token - no user is involved). - The MCP client includes the token as
Authorization: Bearer <token>on all requests.
This is the recommended flow for CI/CD pipelines, microservices, and automated agents that do not act on behalf of a specific user.
Code Example
import hashlib
import base64
import os
def generate_pkce_pair():
# Generate a random 32-byte verifier
verifier = base64.urlsafe_b64encode(os.urandom(32)).decode('utf-8').rstrip('=')
# Create the S256 challenge
sha256_hash = hashlib.sha256(verifier.encode('utf-8')).digest()
challenge = base64.urlsafe_b64encode(sha256_hash).decode('utf-8').rstrip('=')
return verifier, challenge
# Example usage for MCP Client initialization
code_verifier, code_challenge = generate_pkce_pair()
print(f"Verifier: {code_verifier}")
print(f"Challenge: {code_challenge}")
Verifier: [random_string] Challenge: [hashed_base64_string]
import os
from msal import PublicClientApplication
# Configuration from environment variables
TENANT_ID = os.environ.get("AZURE_TENANT_ID")
CLIENT_ID = os.environ.get("AZURE_CLIENT_ID")
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPES = ["https://mcp-server.api/Tools.Execute"]
app = PublicClientApplication(CLIENT_ID, authority=AUTHORITY)
def get_access_token():
# Attempt to get token from cache first
accounts = app.get_accounts()
if accounts:
result = app.acquire_token_silent(SCOPES, account=accounts[0])
if result:
return result['access_token']
# Fallback to interactive flow with PKCE (MSAL handles PKCE internally)
result = app.acquire_token_interactive(scopes=SCOPES)
if "access_token" in result:
return result['access_token']
else:
raise Exception(f"Auth failed: {result.get('error_description')}")
token = get_access_token()
print(f"Acquired Token: {token[:10]}...")
Acquired Token: eyJ0eXAiOi...