php-sdk/examples/server/oauth-keycloak/README.md
Volodymyr Panivko e3f4b88768
[Server] Oauth2 based on middleware (#221)
* 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>
2026-03-10 17:10:07 +01:00

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
```