Integrating Umbraco with Azure Private Blob Storage

Discover a comprehensive guide on setting up Umbraco v8 with Azure Private Storage, emphasizing security best practices and step-by-step integration insights.

Why Choose Private Blob Storage Over Public Access?

Azure provides the flexibility to set your container as either public or private.

While public access might seem convenient, it comes with potential security threats.

Microsoft's advice is clear on this:

When a container is configured for public access, any client can read data in that container. Public access presents a potential security risk, so if your scenario does not require it, Microsoft recommends that you disallow it for the storage account.

Microsoft

For in-depth insights on challenges and solutions related to using Umbraco with Private Azure blob containers, this discussion thread is highly recommended.

As James Jackson-South suggested:

You’d need to create your own IImageService implementation since CloudImageService is essentially a HttpRequest

Once you go through the whole discussion, you will notice that you need to take two essential steps to make Umbraco deal with private blob storage:

  • Implement custom IImageService
  • Update the security.config file

Prerequisites: Environment and Libraries

Before diving into the integration, it's crucial to understand the specific environment I worked in.

Sharing the specifics of my project setup provides a clear foundation for anyone looking to replicate this integration.

It ensures a smoother experience by minimizing potential compatibility issues or uncertainties related to version discrepancies.

Here's the working setup of libraries and versions I used:

  • ImageProcessor: 2.7.0.100
  • ImageProcessor.Web: 4.10.0.100
  • ImageProcessor.Web.Plugins.AzureBlobCache: 1.5.0.100
  • WindowsAzure.Storage: 8.7.0
  • UmbracoFileSystemProviders.Azure: 2.0.0-alpha1
  • UmbracoCms: 8.6.0
  • .NET Framework: 4.7.2

Implementing custom Azure Image Service

To retrieve images from Azure securely, a custom IImageService implementation is essential.

The linchpin of this service is the GetImage(object id) method. 

Ensure the blob name excludes the container as a prefix and follows the “3gikqq22/example-image.png” format.

Here's a blueprint of the new AzureImageService:

/// <summary>
/// An image service for retrieving images from Azure.
/// </summary>
public class AzureImageService : IImageService
{
   private CloudBlobContainer _blobContainer;
   private CloudStorageAccount _storageAccount;
   private Dictionary<string, string> _settings = new Dictionary<string, string>();

   /// <summary>
   /// Gets or sets the prefix for the given implementation.
   /// <remarks>
   /// This value is used as a prefix for any image requests that should use this service.
   /// </remarks>
   /// </summary>
   public string Prefix { get; set; } = string.Empty;

   /// <summary>
   /// Gets a value indicating whether the image service requests files from
   /// the locally based file system.
   /// </summary>
   public bool IsFileLocalService => false;

   /// <summary>
   /// Gets or sets any additional settings required by the service.
   /// </summary>
   public Dictionary<string, string> Settings
   {
      get => this._settings;
      set
      {
         this._settings = value;
         this.InitService();
      }
   }

   /// <summary>
   /// Gets or sets the white list of <see cref="Uri" />. 
   /// </summary>
   public Uri[] WhiteList { get; set; }

   /// <summary>
   /// Gets the image using the given identifier.
   /// </summary>
   /// <param name="id"></param>
   /// <returns></returns>
   public async Task<byte[]> GetImage(object id)
   {
      if (await _blobContainer.ExistsAsync())
      {
         //expecting id as "3gikqq22/example-image.png"
         string sId = PrepareBlobName(id);

         CloudBlockBlob blob = _blobContainer.GetBlockBlobReference(sId);

         if (await blob.ExistsAsync())
         {
            using (MemoryStream memoryStream = MemoryStreamPool.Shared.GetStream())
            {
               await blob.DownloadToStreamAsync(memoryStream).ConfigureAwait(false);
               return memoryStream.ToArray();
            }
         }
      }

      return null;
   }

   /// <summary>
   /// Removes container prefix from blob path
   /// </summary>
   /// <param name="id"></param>
   /// <returns></returns>
   private string PrepareBlobName(object id)
   {
      string sId = id.ToString();

      if (sId.StartsWith($"/{this.Settings["Container"]}/"))
      {
         return sId.Substring(this.Settings["Container"].Length + 2);
      }

      return sId;
   }


   /// <summary>
   /// Gets a value indicating whether the current request passes sanitizing rules.
   /// </summary>
   /// <param name="path">The image path.</param>
   /// <returns>
   /// <c>True</c> if the request is valid; otherwise, <c>False</c>.
   /// </returns>
   public bool IsValidRequest(string path) => ImageHelpers.IsValidImageExtension(path);

   /// <summary>
   /// Initialise the service.
   /// </summary>
   private void InitService()
   {
      // Retrieve storage accounts from connection string.
      _storageAccount = CloudStorageAccount.Parse(this.Settings["StorageAccount"]);

      // Create the blob client.
      CloudBlobClient blobClient = _storageAccount.CreateCloudBlobClient();

      string container = this.Settings.ContainsKey("Container")
         ? this.Settings["Container"]
         : string.Empty;

      BlobContainerPublicAccessType accessType = this.Settings.ContainsKey("AccessType")
         ? (BlobContainerPublicAccessType)Enum.Parse(typeof(BlobContainerPublicAccessType), this.Settings["AccessType"])
         : BlobContainerPublicAccessType.Blob;

      this._blobContainer = CreateContainer(blobClient, container, accessType);
   }

   /// <summary>
   /// Returns the cache container, creating a new one if none exists.
   /// </summary>
   /// <param name="cloudBlobClient"><see cref="CloudBlobClient"/> where the container is stored.</param>
   /// <param name="containerName">The name of the container.</param>
   /// <param name="accessType"><see cref="BlobContainerPublicAccessType"/> indicating the access permissions.</param>
   /// <returns>The <see cref="CloudBlobContainer"/></returns>
   private static CloudBlobContainer CreateContainer(CloudBlobClient cloudBlobClient, string containerName, BlobContainerPublicAccessType accessType)
   {
      CloudBlobContainer container = cloudBlobClient.GetContainerReference(containerName);

      if (!container.Exists())
      {
         container.Create();
         container.SetPermissions(new BlobContainerPermissions { PublicAccess = accessType });
      }

      return container;
   }
}

For those interested in accessing the complete source code, it's available on GitHub.

Configuring the Security for ImageProcessor

After implementing the Azure Image Service, the next step is configuring the security settings.

Locate the ~/config/imageprocessor/security.config file and define the new service as shown:

Imageprocessor security config file in Umbraco project location

Imageprocessor security config file in Umbraco project location

Below configuration provides essential details on how the "AzureImageService" interacts with Azure Blob Storage, specifically regarding storage account connection, container name, and access level.

<?xml version="1.0" encoding="utf-8"?>
<security>
	<services>		
		<service name="AzureImageService" type="[Namespace].AzureImageService, [Namespace]">
			<settings>
				<setting key="StorageAccount" value="[StorageAccountConnectionString]" />
				<setting key="Container" value="media" />
				<setting key="AccessType" value="Off" />
			</settings>
		</service>
	</services>
</security>

Let's break it down:

<service name="AzureImageService" type="[Namespace].AzureImageService, [Namespace]">: This element defines the service named "AzureImageService". Its implementation is specified in a given namespace, represented by [Namespace].

<settings>: This element encompasses configuration settings tailored for the "AzureImageService".

<setting key="StorageAccount" value="[StorageAccountConnectionString]" />: Indicates the connection string for the Azure storage account.

<setting key="Container" value="media" />: Sets the Azure Blob Storage container's name as "media".

<setting key="AccessType" value="Off" />: Designates the container's accessibility as "Off". This means access is granted only with appropriate authorization.

With this configuration in place, ImageProcessor will adapt its operation to retrieve media files directly from Azure Blob Storage.

Conclusion on Azure Private Blob Storage and Umbraco

Setting up Umbraco to work securely with Azure Private Blob Storage may seem intricate, but with the proper guidance, it becomes straightforward.

By opting for private storage, you're not only aligning with Microsoft's security recommendations but also ensuring that your website's media content remains protected from potential threats.

Need Assistance? Feel free to contact us for any questions or further assistance.

For more insightful articles, don't forget to browse our blog!

Integrating Umbraco with Azure Private Blob Storage Comments

Big thanks for the comments (restored from the old blog)

↑ Top ↑