attributes->get('tenant_id'); if (! $tenantId) { return $this->missingTenantIdResponse(); } // Get the tenant from request (query param, route param, or header) $requestTenantId = $this->getTenantIdFromRequest($request); if ($requestTenantId && $requestTenantId != $tenantId) { return $this->tenantIsolationViolationResponse((int) $tenantId, (int) $requestTenantId); } // Set tenant context for query scoping $connection = DB::connection(); if (in_array($connection->getDriverName(), ['mysql', 'mariadb'])) { $connection->statement('SET @tenant_id = ?', [$tenantId]); } // Add tenant context to request for easy access in controllers $request->attributes->set('current_tenant_id', $tenantId); return $next($request); } /** * Extract tenant ID from request */ private function getTenantIdFromRequest(Request $request): ?int { // 1. Route parameter (e.g., /api/v1/tenant/123/events) if ($request->route('tenant')) { return (int) $request->route('tenant'); } // 2. Query parameter (e.g., ?tenant_id=123) if ($request->query('tenant_id')) { return (int) $request->query('tenant_id'); } // 3. Header (X-Tenant-ID) if ($request->header('X-Tenant-ID')) { return (int) $request->header('X-Tenant-ID'); } // 4. For tenant-specific resources, use token tenant_id return null; } private function missingTenantIdResponse(): JsonResponse { return ApiError::response( 'tenant_context_missing', 'Tenant Context Missing', 'Tenant ID not found in access token.', Response::HTTP_UNAUTHORIZED ); } private function tenantIsolationViolationResponse(int $tokenTenantId, int $requestTenantId): JsonResponse { return ApiError::response( 'tenant_isolation_violation', 'Tenant Isolation Violation', 'The requested resource belongs to a different tenant.', Response::HTTP_FORBIDDEN, [ 'token_tenant_id' => $tokenTenantId, 'request_tenant_id' => $requestTenantId, ] ); } }