[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>
This commit is contained in:
parent
61c20f1450
commit
e3f4b88768
1 changed files with 6038 additions and 0 deletions
135
examples/server/oauth-keycloak/README.md
Normal file
135
examples/server/oauth-keycloak/README.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# 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
|
||||
```
|
||||
Loading…
Reference in a new issue