* 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>
135 lines
4.5 KiB
Markdown
135 lines
4.5 KiB
Markdown
# OAuth Keycloak Example
|
|
|
|
This example demonstrates MCP server authorization using Keycloak as the OAuth 2.0 / OpenID Connect provider.
|
|
|
|
## Features
|
|
|
|
- JWT token validation with automatic JWKS discovery
|
|
- Protected Resource Metadata (RFC 9728) at `/.well-known/oauth-protected-resource`
|
|
- MCP tools protected by OAuth authentication
|
|
- Pre-configured Keycloak realm with test user
|
|
|
|
## Quick Start
|
|
|
|
1. **Start the services:**
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
2. **Wait for Keycloak to be ready** (may take 30-60 seconds):
|
|
|
|
```bash
|
|
docker compose logs -f keycloak
|
|
# Wait until you see "Running the server in development mode"
|
|
```
|
|
|
|
3. **Get an access token:**
|
|
|
|
```bash
|
|
# Using Resource Owner Password Credentials (for testing only)
|
|
TOKEN=$(curl -s -X POST "http://localhost:8180/realms/mcp/protocol/openid-connect/token" \
|
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
-d "client_id=mcp-client" \
|
|
-d "username=demo" \
|
|
-d "password=demo123" \
|
|
-d "grant_type=password" \
|
|
-d "scope=openid mcp" | jq -r '.access_token')
|
|
|
|
echo $TOKEN
|
|
```
|
|
|
|
4. **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"}}}'
|
|
```
|
|
|
|
5. **Use with MCP Inspector:**
|
|
|
|
MCP Inspector can call this server if you provide a valid Bearer token manually (Authorization header). It does not run the OAuth login flow automatically.
|
|
|
|
## Keycloak Configuration
|
|
|
|
The realm is pre-configured with:
|
|
|
|
| Item | Value |
|
|
|------|-------|
|
|
| Realm | `mcp` |
|
|
| Client (public) | `mcp-client` |
|
|
| Client (resource) | `mcp-server` |
|
|
| Test User | `demo` / `demo123` |
|
|
| Scopes | `mcp:read`, `mcp:write` |
|
|
|
|
### Keycloak Admin Console
|
|
|
|
Access at http://localhost:8180/admin with:
|
|
- Username: `admin`
|
|
- Password: `admin`
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ MCP Client │────▶│ Nginx │────▶│ PHP-FPM │
|
|
│ │ │ (port 8000) │ │ MCP Server │
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
│ │
|
|
│ Get Token │ Validate JWT
|
|
▼ ▼
|
|
┌─────────────────┐ ┌─────────────────┐
|
|
│ Keycloak │◀───────────────────────────│ JWKS Fetch │
|
|
│ (port 8180) │ │ │
|
|
└─────────────────┘ └─────────────────┘
|
|
```
|
|
|
|
## Files
|
|
|
|
- `docker-compose.yml` - Docker Compose configuration
|
|
- `Dockerfile` - PHP-FPM container with dependencies
|
|
- `nginx/default.conf` - Nginx configuration for MCP endpoint
|
|
- `keycloak/mcp-realm.json` - Pre-configured Keycloak realm
|
|
- `server.php` - MCP server with OAuth middleware
|
|
- `McpElements.php` - MCP tools and resources
|
|
|
|
## Configuration
|
|
|
|
This example uses hard-coded values in `server.php` for consistency with other examples:
|
|
- Keycloak external URL: `http://localhost:8180`
|
|
- Keycloak internal URL: `http://keycloak:8180`
|
|
- Realm: `mcp`
|
|
- Audience: `mcp-server`
|
|
|
|
## Troubleshooting
|
|
|
|
### Token validation fails
|
|
|
|
1. Ensure Keycloak is fully started (check health endpoint)
|
|
2. Verify the token hasn't expired (default: 5 minutes)
|
|
3. Check that the audience claim matches `mcp-server`
|
|
|
|
### Connection refused
|
|
|
|
1. Wait for Keycloak health check to pass
|
|
2. Check Docker network connectivity: `docker compose logs`
|
|
|
|
### JWKS fetch fails
|
|
|
|
The MCP server needs to reach Keycloak at `http://keycloak:8180` (Docker network).
|
|
For local development outside Docker, use `http://localhost:8180`.
|
|
|
|
## Cleanup
|
|
|
|
```bash
|
|
docker compose down -v
|
|
```
|