php-mcp/src/Mcp/Middleware/McpAuthenticate.php
Snider 6f309979de refactor: move MCP module from Core\Mod\Mcp to Core\Mcp namespace
Relocates the MCP module to a top-level namespace as part of the
monorepo separation, removing the intermediate Mod directory layer.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:26:14 +00:00

102 lines
3 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mcp\Middleware;
use Core\Mod\Tenant\Models\Workspace;
use Core\Mod\Tenant\Services\EntitlementService;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* MCP Portal Authentication Middleware.
*
* Handles authentication for the MCP portal:
* - Public routes (landing, server list) pass through
* - Protected routes require auth or API key
* - Checks mcp.access entitlement for workspace
*/
class McpAuthenticate
{
public function __construct(
protected EntitlementService $entitlementService
) {}
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next, string $level = 'optional'): Response
{
// Try API key auth first (for programmatic access)
$workspace = $this->authenticateByApiKey($request);
// Fall back to session auth
if (! $workspace && $request->user()) {
$user = $request->user();
if (method_exists($user, 'defaultHostWorkspace')) {
$workspace = $user->defaultHostWorkspace();
}
}
// Store workspace for downstream use
if ($workspace) {
$request->attributes->set('mcp_workspace', $workspace);
// Check MCP access entitlement
$result = $this->entitlementService->can($workspace, 'mcp.access');
$request->attributes->set('mcp_entitlement', $result);
}
// For 'required' level, must have workspace
if ($level === 'required' && ! $workspace) {
return $this->unauthenticatedResponse($request);
}
return $next($request);
}
/**
* Authenticate using API key from header or query.
*/
protected function authenticateByApiKey(Request $request): ?Workspace
{
$apiKey = $request->header('X-API-Key')
?? $request->header('Authorization')
?? $request->query('api_key');
if (! $apiKey) {
return null;
}
// Strip 'Bearer ' prefix if present
if (str_starts_with($apiKey, 'Bearer ')) {
$apiKey = substr($apiKey, 7);
}
// Look up workspace by API key
return Workspace::whereHas('apiKeys', function ($query) use ($apiKey) {
$query->where('key', hash('sha256', $apiKey))
->where(function ($q) {
$q->whereNull('expires_at')
->orWhere('expires_at', '>', now());
});
})->first();
}
/**
* Return unauthenticated response.
*/
protected function unauthenticatedResponse(Request $request): Response
{
if ($request->expectsJson() || $request->is('api/*')) {
return response()->json([
'error' => 'unauthenticated',
'message' => 'Authentication required. Provide an API key or sign in.',
], 401);
}
return redirect()->guest(route('login'));
}
}