Enterprise authorization for MCP on Azure App Service
This repository separates two ideas that are easy to conflate:
- What you can deploy with Microsoft Entra ID and App Service Authentication today: centrally governed OAuth, protected resource metadata (PRM), known-client preauthorization, Conditional Access, and platform token validation.
- What the MCP Enterprise-Managed Authorization (EMA) extension adds: an enterprise identity provider issues an Identity Assertion JWT Authorization Grant (ID-JAG), which a resource authorization server exchanges for an MCP access token without a per-server browser flow.
The first path is deployable to Azure. The second is a local, specification-accurate interoperability lab.
[!IMPORTANT]Microsoft Entra preauthorization is not the EMA ID-JAG extension. Current App Service MCP authorization guidance documents standard OAuth and preauthorization. The stable EMA specification additionally requires RFC 8693 ID-JAG issuance and RFC 7523 assertion exchange.
What is included
| Path | Purpose | Runs where |
|---|---|---|
| Entra + App Service Authentication | Secure a real MCP endpoint with Entra OAuth, PRM, client allow-listing, and platform token validation | Azure App Service |
| EMA ID-JAG lab | Exercise the full client → enterprise IdP → resource authorization server → MCP server flow | Local process |
The deployed MCP server exposes two read-only tools:
whoami: returns the principal that App Service Authentication validated.authorization_model: reports which authorization path protects the deployment and explicitly states that it is not the ID-JAG flow.
The local EMA lab exposes whoami after exchanging a single-use ID-JAG for an audience-restricted MCP access token.
Architecture
Deployable Entra path
MCP client
│ discover PRM + request API scope
▼
Microsoft Entra ID
│ user/app policy, preauthorization, Conditional Access
▼
App Service Authentication
│ validate issuer, audience, lifetime, tenant, and client application
▼
Python MCP server on App Service
This is centrally managed enterprise OAuth. It removes consent prompts for preauthorized clients, but it does not issue or exchange an ID-JAG.
EMA reference path
MCP client
│ enterprise sign-in assertion
▼
Enterprise IdP
│ RFC 8693: issue short-lived ID-JAG after policy evaluation
▼
MCP authorization server
│ RFC 7523: validate ID-JAG and issue resource-bound access token
▼
MCP server
The lab validates the ID-JAG signature, typ, issuer, audience, client, resource, scopes, expiry, and replay identifier before issuing an access token. It deliberately requires the optional resource claim so every issued token is bound to an explicit MCP resource.
Prerequisites
- Python 3.11 or later
- Azure CLI
- Azure Developer CLI
jq- Permission to create a Microsoft Entra app registration
- An Azure subscription
The Azure sample defaults to Python 3.14 on Linux App Service and a Basic B1 plan.
Run locally
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
python -m uvicorn ema_app_service.app:app --app-dir src --reload --port 8000
The MCP endpoint is http://localhost:8000/mcp. App Service Authentication is not present locally, so the whoami tool intentionally fails without a simulated platform principal header. The authorization_model tool remains usable.
Run the tests:
python -m ruff check .
python -m pytest
Run the EMA ID-JAG lab
python -m examples.ema_lab.client
Expected result:
{'subject': '[email protected]', 'client_id': 'finance-agent',
'scopes': ['mcp:whoami'], 'resource': 'http://localhost/mcp'}
The lab runs the stand-in IdP, MCP authorization server, MCP resource server, and client in one process. It uses a clearly marked demo signing key and in-memory token state. It is a protocol reference, not a production identity provider.
Why the SDK is pinned to Git
The current PyPI mcp release, 1.28.1, does not yet contain the identity-assertion client and authorization-server APIs used by the stable EMA extension. This repository pins the official SDK to commit:
2713b53b127afc094dc97d6067df9f69b647661c
Replace the Git pin with a released package version after those APIs ship and the tests pass unchanged.
Deploy to Azure
The included azd/Bicep deployment creates:
- Linux App Service plan, Basic B1
- Python 3.14 App Service with system-assigned managed identity
- App Service Authentication with Microsoft Entra ID
- MCP protected resource metadata
- Log Analytics and workspace-based Application Insights with Microsoft Entra-authenticated ingestion through the app's managed identity
The idempotent Entra setup script creates or reuses a dedicated single-tenant app registration, exposes user_impersonation, and preauthorizes:
- Visual Studio Code:
aebc6443-996d-45c2-90f0-388ff96faa56 - Azure CLI:
04b07795-8ddb-461a-bbee-02f9e1bf7b46
Deploy:
azd auth login
azd env new ema-mcp
azd env set AZURE_SUBSCRIPTION_ID <subscription-id>
azd env set AZURE_LOCATION eastus2
./scripts/configure-entra-app.sh
azd up
The setup script runs explicitly once because azd resolves required Bicep parameters before invoking preprovision. The hook then reruns the same idempotent script on subsequent provisions.
Some enterprise tenants require a Service Tree reference on new app registrations:
azd env set AZURE_SERVICE_MANAGEMENT_REFERENCE <service-tree-id>
./scripts/configure-entra-app.sh
azd up
Get the endpoint:
azd env get-value MCP_URL
Add that URL to .vscode/mcp.json, start the server, and complete the Microsoft sign-in flow.
What App Service Authentication enforces
The Bicep configuration:
- Requires authentication on
/mcp. - Returns
401rather than redirecting nonbrowser clients. - Publishes PRM with
api://<app-id>/user_impersonation. - Accepts only the expected API audience.
- Allows only the preconfigured VS Code and Azure CLI client applications.
- Injects the validated principal for the application to inspect.
- Leaves
/and/healthpublic for status and health checks.
The sample never treats a caller-supplied bearer token or arbitrary header as proof of identity. App Service Authentication is the enforcement point; whoami only reports the platform-injected principal.
Conditional Access and governance
Conditional Access is evaluated when Entra issues the resource token. Use tenant policy to require controls such as MFA, compliant devices, trusted locations, or authentication strength for the MCP API.
This sample does not create Conditional Access policies automatically because they are tenant-wide security controls with licensing and rollout implications. An administrator should:
- Target the MCP enterprise application or relevant users/groups.
- Start in report-only mode.
- Review Entra sign-in logs.
- Exclude emergency access accounts.
- Enable the policy after validating impact.
App Service Authentication gates the server as a whole. Implement tool-specific authorization in application code when different tools require different roles or scopes.
Verify the deployment
After the app has warmed up:
./scripts/verify-deployment.sh
The script checks:
- PRM is available.
- An unauthenticated MCP call returns
401. - Azure CLI can obtain an API token.
- The same MCP request succeeds with that token.
Use Visual Studio Code to invoke whoami and confirm the returned tenant, client application, subject, roles, and scopes.
Query requests in Application Insights:
requests
| where timestamp > ago(15m)
| where url contains "/mcp"
| project timestamp, resultCode, name, operation_Id
| order by timestamp desc
Security boundaries
- Never forward the MCP resource token to a downstream API. Use managed identity or an explicit on-behalf-of flow for downstream access.
- The local IdP and in-memory authorization server are demonstration components only.
- ID-JAGs are short-lived and single-use; the lab rejects
jtireplay. - Production validators should verify ID-JAG signatures against the enterprise IdP's published JWKS. The lab's shared HMAC key is demo-only.
- No refresh token is issued by the EMA authorization server. The enterprise IdP retains control by deciding whether to issue the next ID-JAG.
- The Basic B1 sample has a public network endpoint protected by authentication. Add private networking and a gateway for production deployments that require network isolation.
Clean up
azd down --purge
azd down removes Azure resources but not the Microsoft Entra app registration created by the preprovision hook. Remove that registration separately after confirming no environment still uses it.
Sources
- Enterprise-Managed Authorization specification
- Enterprise-Managed Authorization announcement
- MCP Python SDK identity assertion guide
- Configure built-in MCP server authorization on App Service
- Secure MCP calls to App Service from Visual Studio Code
License
MIT