* Add Middleware handlers to StreamableHttpTransport * OAuth Implementation based on middleware * OAuth Implementation based on middleware * Add Middleware handlers to StreamableHttpTransport * OAuth Implementation based on middleware * OAuth Implementation based on middleware Signed-off-by: Volodymyr Panivko <sveneld300@gmail.com> * Address PR review feedback for OAuth middleware * Apply php-cs-fixer to satisfy QA * Use Mcp exception classes in OAuth middleware * Refactor OAuth HTTP stack and add Microsoft example policies * Fix QA PHPStan findings in OAuth examples and auth middleware * Require OIDC discovery in JWKS provider and refine OAuth proxy behavior * Remove local development docker artifacts from VCS * Revert .gitignore changes for local docker files * Fix body retrieval in OAuthRequestMetaMiddleware for proper string conversion Signed-off-by: Volodymyr Panivko <sveneld300@gmail.com> * Address OAuth PR review feedback and align example configs * Fix QA style issues and add authorization docs * Fix PHPStan typing issues in OAuth middleware * Remove duplicate Middleware namespace, consolidate under Http\Middleware Remove old Mcp\Server\Transport\Middleware\ classes that were duplicated in Mcp\Server\Transport\Http\Middleware\ and Http\OAuth\. Update docs to reference the correct namespaces. * Address PR review: replace Throwable catch with specific exceptions, add JSON_THROW_ON_ERROR - JwtTokenValidator: replace catch(\Throwable) with explicit \InvalidArgumentException|\UnexpectedValueException|\DomainException, letting RuntimeException from JwksProvider propagate as infrastructure errors - OAuthProxyMiddleware: add \JSON_THROW_ON_ERROR to json_encode for metadata response - Update tests to expect RuntimeException for JWKS infrastructure failures --------- Signed-off-by: Volodymyr Panivko <sveneld300@gmail.com> Co-authored-by: Volodymyr Panivko <volodymyr.panivko@togethernetworks.com>
7.4 KiB
OAuth Microsoft Entra ID Example
This example demonstrates MCP server authorization using Microsoft Entra ID (formerly Azure AD) as the OAuth 2.0 / OpenID Connect provider.
Features
- 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
Prerequisites
- Azure Subscription with access to Entra ID
- App Registration in Azure Portal
Azure Setup
1. Create App Registration
- 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
2. Configure the App
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
3. Create a Client App (for testing)
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
Quick Start
- 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"}}}'
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ MCP Client │────▶│ Nginx │────▶│ PHP-FPM │
│ │ │ (port 8000) │ │ MCP Server │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ Get Token │ Validate JWT
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Microsoft │◀───────────────────────────│ JWKS Fetch │
│ Entra ID │ │ │
└─────────────────┘ └─────────────────┘
│
│ (Optional) Graph API
▼
┌─────────────────┐
│ Microsoft │
│ Graph API │
└─────────────────┘
Files
docker-compose.yml- Docker Compose configurationDockerfile- PHP-FPM containernginx/default.conf- Nginx configurationenv.example- Environment variables templateserver.php- MCP server with OAuth middlewareMicrosoftJwtTokenValidator.php- Example-specific validator for Graph/non-Graph tokensMicrosoftOidcMetadataPolicy.php- Lenient metadata validation policyMcpElements.php- MCP tools including Graph API integration
Environment Variables
| 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 Token Structure
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 |
Troubleshooting
"Invalid issuer" error
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.
"Invalid audience" error
The aud claim must match AZURE_CLIENT_ID. For v2.0 tokens with custom scopes,
the audience might be api://your-client-id.
JWKS fetch fails
Microsoft's JWKS endpoint is public. Ensure your container can reach:
https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys
code_challenge_methods_supported missing in discovery metadata
This example configures OidcDiscovery with MicrosoftOidcMetadataPolicy, so this
field can be missing or malformed and will not fail discovery.
Graph API errors
- Ensure
AZURE_CLIENT_SECRETis set - Verify API permissions have admin consent
- Check that the user exists in your tenant
Security Notes
- 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.
Cleanup
docker compose down -v