php-sdk/examples/server/oauth-microsoft/README.md
simon chrzanowski 863cfc1e78
[Server] feat: relax StrictOidcDiscoveryMetadataPolicy and add Dynamic Client Registration middleware (RFC 7591) (#269)
* feat: relax OIDC discovery policy and add Dynamic Client Registration middleware (RFC 7591)

- Add LenientOidcDiscoveryMetadataPolicy for providers that omit
  code_challenge_methods_supported (e.g. FusionAuth, Microsoft Entra ID)
- Keep StrictOidcDiscoveryMetadataPolicy RFC-aligned
- Add ClientRegistrationMiddleware handling POST /register and enriching
  /.well-known/oauth-authorization-server with registration_endpoint
- Update Microsoft example to use built-in LenientOidcDiscoveryMetadataPolicy

* fix: move metadata policy note under server.php in Files section

Address review comment: the LenientOidcDiscoveryMetadataPolicy bullet
was a behavioral note, not a file entry. Inline it into the server.php
description to keep the Files list consistent.

* fix: address Copilot review comments on ClientRegistrationMiddleware

- Decode JSON with assoc=true for registrar data so nested objects
  are associative arrays, not stdClass instances
- Add Cache-Control: no-store to all error responses (400), not just
  the success response (201)
- Rewind response body stream before returning unmodified response
  when metadata is not valid JSON, preventing empty body on read

* fix: harden ClientRegistrationMiddleware and improve test coverage

- Revert spurious .gitignore trailing newline
- Use JSON_THROW_ON_ERROR consistently in enrichAuthServerMetadata()
- Add comment clarifying the second json_decode in handleRegistration()
- Assert Cache-Control: no-store on registrar exception error response
- Add test for non-object JSON body in metadata enrichment path
- Extract createPlainTextHandler helper for non-JSON response tests

* fix: reject JSON array bodies in metadata enrichment

Treat JSON list arrays (e.g. [] or ["..."]) as invalid metadata in
enrichAuthServerMetadata() to prevent corrupting the response shape
by adding registration_endpoint to a non-object.

* fix: add RFC 7591 error codes, Content-Type validation, and docblock safety warning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:27:27 +02:00

223 lines
7.6 KiB
Markdown

# 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
1. **Azure Subscription** with access to Entra ID
2. **App Registration** in Azure Portal
## Azure Setup
### 1. Create App Registration
1. Go to [Azure Portal](https://portal.azure.com) > **Entra ID** > **App registrations**
2. Click **New registration**
3. Configure:
- **Name**: `MCP Server`
- **Supported account types**: Choose based on your needs
- **Redirect URI**: Leave empty for now (this is a resource server)
4. Click **Register**
### 2. Configure the App
After registration:
1. **Copy values for `.env`**:
- **Application (client) ID** → `AZURE_CLIENT_ID`
- **Directory (tenant) ID** → `AZURE_TENANT_ID`
2. **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`
3. **Create client secret** (for Graph API calls):
- Go to **Certificates & secrets**
- Click **New client secret**
- Copy the secret value → `AZURE_CLIENT_SECRET`
4. **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:
1. **New registration**:
- **Name**: `MCP Client`
- **Redirect URI**: `http://localhost` (Public client/native)
2. **Authentication**:
- Enable **Allow public client flows** for PKCE
3. **API permissions**:
- Add permission to your MCP Server app's exposed API
## Quick Start
1. **Copy environment file:**
```bash
cp env.example .env
```
2. **Edit `.env` with your Azure values:**
```bash
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret # Optional, for Graph API
```
3. **Start the services:**
```bash
docker compose up -d
```
4. **Get an access token:**
Using Azure CLI:
```bash
# 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.
5. **Test the MCP server:**
```bash
# 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 configuration
- `Dockerfile` - PHP-FPM container
- `nginx/default.conf` - Nginx configuration
- `env.example` - Environment variables template
- `server.php` - MCP server with OAuth middleware (uses built-in `LenientOidcDiscoveryMetadataPolicy` for metadata validation)
- `MicrosoftJwtTokenValidator.php` - Example-specific validator for Graph/non-Graph tokens
- `McpElements.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
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).
### Graph API errors
1. Ensure `AZURE_CLIENT_SECRET` is set
2. Verify API permissions have admin consent
3. Check that the user exists in your tenant
## Security Notes
1. **Never commit `.env` files** - they contain secrets
2. **Use managed identities** in Azure deployments instead of client secrets
3. **Implement proper token refresh** in production clients
4. **Validate scopes** for sensitive operations
5. **Important:** `MicrosoftJwtTokenValidator` in this example accepts `nonce` Graph-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
```bash
docker compose down -v
```