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

4.5 KiB

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:
docker compose up -d
  1. Wait for Keycloak to be ready (may take 30-60 seconds):
docker compose logs -f keycloak
# Wait until you see "Running the server in development mode"
  1. Get an access token:
# 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
  1. 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"}}}'
  1. 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

docker compose down -v