[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
134
examples/server/oauth-keycloak/McpElements.php
Normal file
134
examples/server/oauth-keycloak/McpElements.php
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Example\Server\OAuthKeycloak;
|
||||
|
||||
use Mcp\Capability\Attribute\McpPrompt;
|
||||
use Mcp\Capability\Attribute\McpResource;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Mcp\Server\RequestContext;
|
||||
|
||||
/**
|
||||
* MCP elements for the OAuth Keycloak example.
|
||||
*
|
||||
* These tools demonstrate a protected MCP server.
|
||||
* All requests must include a valid OAuth bearer token.
|
||||
*/
|
||||
final class McpElements
|
||||
{
|
||||
/**
|
||||
* Confirms the user is authenticated.
|
||||
*
|
||||
* The fact that this tool executes means the request passed OAuth validation.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
#[McpTool(
|
||||
name: 'get_auth_status',
|
||||
description: 'Confirm authentication status - only accessible with valid OAuth token'
|
||||
)]
|
||||
public function getAuthStatus(RequestContext $context): array
|
||||
{
|
||||
$meta = $context->getRequest()->getMeta() ?? [];
|
||||
$oauth = isset($meta['oauth']) && \is_array($meta['oauth']) ? $meta['oauth'] : [];
|
||||
$claims = isset($oauth['oauth.claims']) && \is_array($oauth['oauth.claims']) ? $oauth['oauth.claims'] : [];
|
||||
$scopes = isset($oauth['oauth.scopes']) && \is_array($oauth['oauth.scopes']) ? $oauth['oauth.scopes'] : [];
|
||||
|
||||
return [
|
||||
'authenticated' => true,
|
||||
'provider' => 'Keycloak',
|
||||
'message' => 'You have successfully authenticated with OAuth!',
|
||||
'timestamp' => date('c'),
|
||||
'user' => [
|
||||
'subject' => $oauth['oauth.subject'] ?? ($claims['sub'] ?? null),
|
||||
'username' => $claims['preferred_username'] ?? null,
|
||||
'name' => $claims['name'] ?? null,
|
||||
'email' => $claims['email'] ?? null,
|
||||
'issuer' => $claims['iss'] ?? null,
|
||||
'audience' => $claims['aud'] ?? null,
|
||||
'scopes' => $scopes,
|
||||
'expires_at' => isset($claims['exp']) && is_numeric($claims['exp'])
|
||||
? date('c', (int) $claims['exp'])
|
||||
: null,
|
||||
],
|
||||
'note' => 'This endpoint is protected by JWT validation. If you see this, your token was valid.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates calling a protected external API.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
#[McpTool(
|
||||
name: 'call_protected_api',
|
||||
description: 'Simulate calling a protected external API endpoint'
|
||||
)]
|
||||
public function callProtectedApi(
|
||||
string $endpoint,
|
||||
string $method = 'GET',
|
||||
): array {
|
||||
// In a real implementation, you would:
|
||||
// 1. Use token exchange to get a token for the downstream API
|
||||
// 2. Or use client credentials with the user's context
|
||||
// 3. Make the actual HTTP call to the protected API
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => \sprintf('Simulated %s request to %s', $method, $endpoint),
|
||||
'simulated_response' => [
|
||||
'data' => 'This is simulated data from the protected API',
|
||||
'timestamp' => date('c'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current server time and status.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
#[McpResource(
|
||||
uri: 'server://status',
|
||||
name: 'server_status',
|
||||
description: 'Current server status (protected resource)',
|
||||
mimeType: 'application/json'
|
||||
)]
|
||||
public function getServerStatus(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'healthy',
|
||||
'timestamp' => date('c'),
|
||||
'php_version' => \PHP_VERSION,
|
||||
'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
|
||||
'protected' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A greeting prompt.
|
||||
*/
|
||||
#[McpPrompt(
|
||||
name: 'greeting',
|
||||
description: 'Generate a greeting message'
|
||||
)]
|
||||
public function greeting(string $style = 'formal'): string
|
||||
{
|
||||
return match ($style) {
|
||||
'casual' => 'Hey there! Welcome to the protected MCP server!',
|
||||
'formal' => 'Good day. Welcome to the OAuth-protected MCP server.',
|
||||
'friendly' => 'Hello! Great to have you here!',
|
||||
default => 'Welcome to the MCP server!',
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue