This example demonstrates MCP server authorization using Microsoft Entra ID (formerly Azure AD) as the OAuth 2.0 / OpenID Connect provider.
- JWT token validation with Microsoft Entra ID
- Microsoft-specific validator/discovery overrides for Entra quirks
- Protected Resource Metadata (RFC 9728)
- MCP tools that access Microsoft claims
- Optional Microsoft Graph API integration
- Azure Subscription with access to Entra ID
- App Registration in Azure Portal
- Go to Azure Portal > Entra ID > App registrations
- Click New registration
- Configure:
- Name:
MCP Server - Supported account types: Choose based on your needs
- Redirect URI: Leave empty for now (this is a resource server)
- Name:
- Click Register
After registration:
-
Copy values for
.env:- Application (client) ID →
AZURE_CLIENT_ID - Directory (tenant) ID →
AZURE_TENANT_ID
- Application (client) ID →
-
Expose an API (optional, for custom scopes):
- Go to Expose an API
- Set Application ID URI (e.g.,
api://your-client-id) - Add scopes like
mcp.read,mcp.write
-
Create client secret (for Graph API calls):
- Go to Certificates & secrets
- Click New client secret
- Copy the secret value →
AZURE_CLIENT_SECRET
-
API Permissions (for Graph API):
- Go to API permissions
- Add Microsoft Graph > Delegated permissions:
User.Read(for profile)Mail.Read(for emails, optional)
- Grant admin consent if required
Create a separate app registration for the client:
-
New registration:
- Name:
MCP Client - Redirect URI:
http://localhost(Public client/native)
- Name:
-
Authentication:
- Enable Allow public client flows for PKCE
-
API permissions:
- Add permission to your MCP Server app's exposed API
- Copy environment file:
cp env.example .env- Edit
.envwith your Azure values:
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret # Optional, for Graph API- Start the services:
docker compose up -d- Get an access token:
Using Azure CLI:
# Login
az login
# Get token for your app
TOKEN=$(az account get-access-token \
--resource api://your-client-id \
--query accessToken -o tsv)Or using MSAL / OAuth flow in your client application.
- Test the MCP server:
# Get Protected Resource Metadata
curl http://localhost:8000/.well-known/oauth-protected-resource
# Call MCP endpoint without token (should get 401)
curl -i http://localhost:8000/mcp
# Call MCP endpoint with token
curl -X POST http://localhost:8000/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ MCP Client │────▶│ Nginx │────▶│ PHP-FPM │
│ │ │ (port 8000) │ │ MCP Server │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ Get Token │ Validate JWT
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Microsoft │◀───────────────────────────│ JWKS Fetch │
│ Entra ID │ │ │
└─────────────────┘ └─────────────────┘
│
│ (Optional) Graph API
▼
┌─────────────────┐
│ Microsoft │
│ Graph API │
└─────────────────┘
docker-compose.yml- Docker Compose configurationDockerfile- PHP-FPM containernginx/default.conf- Nginx configurationenv.example- Environment variables templateserver.php- MCP server with OAuth middleware (uses built-inLenientOidcDiscoveryMetadataPolicyfor metadata validation)MicrosoftJwtTokenValidator.php- Example-specific validator for Graph/non-Graph tokensMcpElements.php- MCP tools including Graph API integration
| Variable | Required | Description |
|---|---|---|
AZURE_TENANT_ID |
Yes | Azure AD tenant ID |
AZURE_CLIENT_ID |
Yes | Application (client) ID |
AZURE_CLIENT_SECRET |
No | Client secret for Graph API calls |
Microsoft Entra ID tokens include these common claims:
| Claim | Description |
|---|---|
oid |
Object ID (unique user identifier in tenant) |
tid |
Tenant ID |
sub |
Subject (unique user identifier) |
name |
Display name |
preferred_username |
Usually the UPN |
email |
Email address (if available) |
upn |
User Principal Name |
Microsoft uses different issuer URLs depending on the token flow:
- v2.0 endpoint (user/delegated flows):
https://login.microsoftonline.com/{tenant}/v2.0 - v1.0 endpoint (client credentials flow):
https://sts.windows.net/{tenant}/
This example automatically accepts both formats by configuring multiple issuers in the MicrosoftJwtTokenValidator.
Check your token's iss claim to verify which format is being used.
The aud claim must match AZURE_CLIENT_ID. For v2.0 tokens with custom scopes,
the audience might be api://your-client-id.
Microsoft's JWKS endpoint is public. Ensure your container can reach:
https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys
The default StrictOidcDiscoveryMetadataPolicy requires code_challenge_methods_supported.
Microsoft Entra ID omits this field despite supporting PKCE with S256.
This example uses the built-in LenientOidcDiscoveryMetadataPolicy which accepts missing
code_challenge_methods_supported (defaults to S256 downstream).
- Ensure
AZURE_CLIENT_SECRETis set - Verify API permissions have admin consent
- Check that the user exists in your tenant
- Never commit
.envfiles - they contain secrets - Use managed identities in Azure deployments instead of client secrets
- Implement proper token refresh in production clients
- Validate scopes for sensitive operations
- Important:
MicrosoftJwtTokenValidatorin this example acceptsnonceGraph-style tokens via claim checks only (iss/exp/nbf) without signature verification. Treat this as demo-only behavior and replace it with full signature validation for production.
docker compose down -v