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>
102 lines
3 KiB
PHP
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'));
|
|
}
|
|
}
|