使用说明
End-to-End MCP Protection with API Management × MCP × OAuth
This repository provides a hands-on learning experience for implementing end-to-end protection using API Management × MCP × OAuth.
Hands-On Overview
This hands-on lab demonstrates the end-to-end flow for securely executing MCP in two patterns:
- AI Agent / Claude Code → APIM → MCP
- VS Code (GitHub Copilot Chat / CLI) → APIM → MCP
AI Agent / Claude Code → APIM → MCP
The overall flow when executing MCP from an AI Agent (e.g., MS Foundry Agent) or Claude Code.
Both clients acquire an access token in advance (via managed identity, service principal or az login) and attach it to requests — the APIM flow is identical.
sequenceDiagram
participant Agent as AI Agent / Claude Code
participant Entra as Entra ID
participant APIM as API Management
participant MCP as MCP Backend (Logic Apps)
Agent->>Entra: 1) Request access token
Entra-->>Agent: 2) Return access token
Agent->>APIM: 3) Access /mcp with access token
Note over APIM: 4) Validate access token
APIM->>Entra: 5a) Acquire token for MCP Backend using APIM Managed Identity
APIM->>MCP: 5b) Access MCP backend with APIM MSI token
MCP-->>APIM: 6a) Return tool list
APIM-->>Agent: 6b) Return tool list
Agent->>APIM: 7) Request to execute MCP tool
Note over APIM: 8) Validate MCP role from access token
alt 9a) Role validation succeeds
APIM->>MCP: 9a-1) Execute MCP tool with APIM MSI token
MCP-->>APIM: 9a-2) Tool execution result
APIM-->>Agent: 9a-3) Return MCP tool execution result
else 9b) Role validation fails
APIM-->>Agent: 403 Forbidden
end
VS Code (GitHub Copilot Chat / CLI) → APIM → MCP
The overall flow when executing MCP from VS Code (GitHub Copilot Chat or CLI).
Both Chat and CLI in VS Code share the same OAuth authorization flow — they discover the OAuth endpoint from APIM and authenticate interactively.
sequenceDiagram
participant VS as VS Code (GitHub Copilot Chat / CLI)
participant APIM as API Management
participant ID as Entra ID
participant MCP as MCP Backend (Logic Apps)
VS->>APIM: 1) Request access to /mcp
APIM-->>VS: 2) 401 Unauthorized
VS->>APIM: 3) GET /.well-known/oauth-protected-resource
APIM-->>VS: 4) Return OAuth metadata
VS->>ID: 5) Get an access token through the login/authorization code flow
VS->>APIM: 6) Re-access /mcp with access token
Note over APIM: 7) Validate access token
APIM->>ID: 8a) Acquire token for MCP Backend using APIM Managed Identity
APIM->>MCP: 8b) Access MCP backend with APIM MSI token
MCP-->>APIM: 9a) Return tool list
APIM-->>VS: 9b) Return tool list
VS->>APIM: 10) Request to execute MCP tool
Note over APIM: 11) Validate MCP role from access token
alt 12a) Role validation succeeds
APIM->>MCP: 12a-1) Execute MCP tool with APIM MSI token
MCP-->>APIM: 12a-2) Tool execution result
APIM-->>VS: 12a-3) Return MCP tool execution result
else 12b) Role validation fails
APIM-->>VS: 403 Forbidden
end
Architecture Features
This hands-on lab implements robust end-to-end security through the following five elements:
1. OAuth2 Authorization
User authentication and access token issuance via Entra ID.
2. Token Validation
APIM validates OAuth access tokens (issuer / audience / signature / expiration) and blocks unauthorized requests.
3. Role-Based Authorization
Evaluates MCP roles (claims) within the access token for fine-grained tool execution control.
4. Secretless Connection
APIM's Managed Identity (MSI) enables secure access to MCP Backend, eliminating credential leakage risks.
5. Backend Protection
MCP Backend enforces authorization at two layers:
- Easy Auth (Entra ID): Platform-level token validation — rejects any request without a valid Entra ID bearer token.
- App Role check (
Mcp.Invoke): Application-level check — rejects callers whose token does not contain theMcp.InvokeApp Role, even if the token itself is valid.
This means only two types of callers can reach the backend directly:
- APIM Managed Identity — holds a permanent
Mcp.Invokerole assignment, used for all production traffic. - Ops Entra ID group members — assigned
Mcp.Invokevia the ops group, for direct-access troubleshooting and testing.
Deploy Hands-On Environment
Deploy the environment using Azure Developer CLI (azd) with the following steps.
Prerequisites
Ensure the following are installed locally:
- Terraform (recommended: v1.5 or later)
- Azure Developer CLI (
azd, recommended: v1.9 or later)
Deployment Steps
1. Deploy
azd up
Delete Resources
azd down
Hands-On
AI Agent / Claude Code → APIM → MCP
Reproduce the sequence of operations where an AI Agent (e.g., MS Foundry Agent) or Claude Code invokes MCP.
Both clients authenticate by acquiring an Azure access token upfront — AI Agent uses Python scripts, Claude Code uses .mcp.json with headersHelper.
sequenceDiagram
participant Agent as AI Agent / Claude Code
participant Entra as Entra ID
participant APIM as API Management
participant MCP as MCP Backend (Logic Apps)
Agent->>Entra: 1) Request access token
Entra-->>Agent: 2) Return access token
Agent->>APIM: 3) Access /mcp with access token
Note over APIM: 4) Validate access token
APIM->>Entra: 5a) Acquire token for MCP Backend using APIM Managed Identity
APIM->>MCP: 5b) Access MCP backend with APIM MSI token
MCP-->>APIM: 6a) Return tool list
APIM-->>Agent: 6b) Return tool list
Agent->>APIM: 7) Request to execute MCP tool
Note over APIM: 8) Validate MCP role from access token
alt 9a) Role validation succeeds
APIM->>MCP: 9a-1) Execute MCP tool with APIM MSI token
MCP-->>APIM: 9a-2) Tool execution result
APIM-->>Agent: 9a-3) Return MCP tool execution result
else 9b) Role validation fails
APIM-->>Agent: 403 Forbidden
end
Steps
AI Agent (Python)
1. Set Up Python Environment
Navigate to the samplecodes/ directory, create and activate a Python virtual environment, and install the required Python packages.
cd samplecodes
python -m venv oauthmcp
source oauthmcp/bin/activate
pip install azure.identity
2. Configure Environment Variables
Set the required values dynamically from the azd environment.
export OAUTH_APP_ID="$(azd env get-value OAUTH_APP_ID)"
export MCP_URL="$(azd env get-value LOGICAPP_MCP_ENDPOINTS)"
3. Verify Access Token
Run check.entraid_token.py and verify that hello_project1 is included in the roles claim.
python check.entraid_token.py | grep -v '^===' \
| jq 'if .roles then (if (.roles | any(. == "hello_project1")) then "✅ OK: hello_project1 is included in roles" else "❌ NG: hello_project1 not found in roles — run az logout && az login" end) else "⚠️ roles claim is missing — run az logout && az login" end'
4. Retrieve MCP Tool List
Run mcp_tool_list.py to verify that the tool list can be retrieved.
python mcp_tool_list.py
5. Execute MCP Tool (Success Case)
Execute the hello_project1 tool and confirm it succeeds.
export MCP_TOOL_NAME="hello_project1"
python mcp_tool_call.py
6. Execute MCP Tool (Rejection Case)
Execute the hello_project2 tool and confirm it is rejected due to lack of permissions.
export MCP_TOOL_NAME="hello_project2"
python mcp_tool_call.py
Claude Code
Claude Code connects to APIM-protected MCP servers using the same token-based flow.
The headersHelper field in .mcp.json calls az account get-access-token, so no interactive browser login is required.
1. Configure .mcp.json
Generate .mcp.json dynamically using azd env get-value.
cd $(git rev-parse --show-toplevel)
LA_URL="$(azd env get-value LOGICAPP_MCP_ENDPOINTS)"
OAUTH_APP_ID="$(azd env get-value OAUTH_APP_ID)"
HELPER="TOKEN=\$(az account get-access-token --scope 'api://${OAUTH_APP_ID}/.default' --query accessToken -o tsv) && echo \"{\\\"Authorization\\\": \\\"Bearer \$TOKEN\\\"}\""
jq -n --arg la_url "$LA_URL" --arg helper "$HELPER" \
'{mcpServers: {
"logic-hello-mcp": {type: "http", url: $la_url, headersHelper: $helper}
}}' \
> .mcp.json
2. Execute MCP Tool (Success Case)
Prompt the following in Claude Code and confirm that the MCP tool executes successfully.
Use logic-hello-mcp to say hello to project1
3. Execute MCP Tool (Rejection Case)
Prompt the following and confirm that execution is rejected due to lack of permissions.
Use logic-hello-mcp to say hello to project2
Tips
Why Azure CLI token instead of OAuth client credentials?
Claude Code supports OAuth via Dynamic Client Registration (DCR), but Entra ID does not. Static client registration would require distributing a client secret to each user, soheadersHelperacquires a token via Azure CLI (az login) instead.Access token expiry and reconnection
Entra ID access tokens are valid for 1 hour.headersHelperruns at startup and on reconnect, and Claude Code also re-runs it and retries once when a tool call returns401or403(e.g., an expired token).Why pre-authorizing Azure CLI is safe
The token is scoped to this hands-on's Entra ID app (api://<OAUTH_APP_ID>), which is single-tenant, so only users who can sign in to this tenant can obtain one. The Terraform resource that enables this is:resource "azuread_application_pre_authorized" "oauth_app" { application_id = azuread_application.oauth_app.id authorized_client_id = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Azure CLI permission_ids = [ azuread_application_permission_scope.user_impersonation.scope_id, ] }
VS Code (GitHub Copilot Chat / CLI) → APIM → MCP
Verify the sequence of operations where VS Code (GitHub Copilot Chat or CLI) invokes MCP.
sequenceDiagram
participant VS as VS Code (GitHub Copilot Chat / CLI)
participant APIM as API Management
participant ID as Entra ID
participant MCP as MCP Backend (Logic Apps)
VS->>APIM: 1) Request access to /mcp
APIM-->>VS: 2) 401 Unauthorized
VS->>APIM: 3) GET /.well-known/oauth-protected-resource
APIM-->>VS: 4) Return OAuth metadata
VS->>ID: 5) Get an access token through the login/authorization code flow
VS->>APIM: 6) Re-access /mcp with access token
Note over APIM: 7) Validate access token
APIM->>ID: 8a) Acquire token for MCP Backend using APIM Managed Identity
APIM->>MCP: 8b) Access MCP backend with APIM MSI token
MCP-->>APIM: 9a) Return tool list
APIM-->>VS: 9b) Return tool list
VS->>APIM: 10) Request to execute MCP tool
Note over APIM: 11) Validate MCP role from access token
alt 12a) Role validation succeeds
APIM->>MCP: 12a-1) Execute MCP tool with APIM MSI token
MCP-->>APIM: 12a-2) Tool execution result
APIM-->>VS: 12a-3) Return MCP tool execution result
else 12b) Role validation fails
APIM-->>VS: 403 Forbidden
end
Steps
GitHub Copilot Chat (agent mode)
1. Start MCP
Start the MCP server. When prompted for Entra ID authentication, authenticate through the browser and verify that the tool list is correctly retrieved.
2. Execute MCP Tool (Success Case)
Prompt the following and confirm that the MCP tool executes successfully.
Use logic-hello-mcp to say hello to project1
3. Execute MCP Tool (Rejection Case)
Prompt the following and confirm that execution is rejected due to lack of permissions.
Use logic-hello-mcp to say hello to project2
GitHub Copilot CLI
1. Start Copilot CLI
Launch the interactive session, specifying the .mcp.json generated in "Configure .mcp.json" under Claude Code above with --additional-mcp-config.
copilot --additional-mcp-config @.mcp.json
Note: If the browser does not open for MCP authorization when running Copilot CLI on WSL, refer to this article.
2. Execute MCP Tool (Success Case)
In the interactive session, send the following message and confirm that the MCP tool executes successfully.
Use logic-hello-mcp to say hello to project1
3. Execute MCP Tool (Rejection Case)
Send the following message and confirm that execution is rejected due to lack of permissions.
Use logic-hello-mcp to say hello to project2
Technical Details and Use Cases
Technical Details and Use Cases
Conclusion
This repository provides a hands-on experience with end-to-end security implementation combining API Management × MCP × OAuth.
A Note on Where This Sits
If you continue on to apim-mcp-a2a-oauth and then APICenter, you'll be walking a three-repository arc that also traces a shape from the tarot's Major Arcana.
This repository already holds both the Hierophant and the Hermit — not one after the other, but side by side. OAuth2, token validation, and role-based authorization rest on official documentation and established patterns: that's the Hierophant, learning what's already agreed upon. But forcing every caller through APIM as a token broker — a managed-identity token swap paired with Easy Auth on the backend, so direct access is closed off — and using PIM for Groups to grant an ops team JIT access, are original combinations of existing building blocks, not copied whole from a single guide. And the finding that APIM and the backend can silently disagree on which of two duplicate JSON keys wins — a possible authorization bypass — was only found by testing the actual deployed backend, not by reading about it anywhere. That's the Hermit, already at work right here.
The next repository carries the same mixture forward, just with the balance shifted further inward. And the one after that gathers all three back together, without mixing them, into a single undivided circle — the World. This repository's part in that is simply to be a solid first outline, so the later stages have something to stand on.