* 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>
223 lines
7.6 KiB
Markdown
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
|
|
```
|