Securing Media In Umbraco

Securing Media In Umbraco article image

This is about securing files in the media section in Umbraco CMS (v7.5.x). There are more than one approach to accomplish this. In this article we will take advantage of the URL routing in ASP.NET MVC to route media to a custom controller.

In short we will:

Create a member group and a member

In the Umbraco Backoffice, click on the Member section and create a new group below Member Groups. In this example I´ve called it AccessToFiles.

 

Create a new member and give it access to the AccessToFiles member group.

 

Property: Access Rights

To define access rights to a specific file or folder in the media section, we extend the File and Folder media types in Umbraco with a Member Group Picker property. A suggestion is to add this to a new tab – Security. Property alias used here is accessRights.

 

Setup Routing

To let the MVC controller handle all media URLs we need to set up route for that.

using System.Web.Mvc;
using System.Web.Routing;
using Umbraco.Core;

namespace MyUmbracoSite.Business
{
    public class StartupHandler : IApplicationEventHandler
    {
        public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
        }

        public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
        }

        public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            // Make sure routing existing files
            RouteTable.Routes.RouteExistingFiles = true;

            // Setup routing for media files
            RouteTable.Routes.MapRoute("MediaAccessRights",
                "media/{id}/{file}",
                new
                {
                    controller = "Media",
                    action = "Index",
                    id = UrlParameter.Optional,
                    file = UrlParameter.Optional
                });
        }
    }
}

Create Controller

The controller used to restrict access to media is a regular MVC controller. It will try to map the information in the path to a media object and check if it has any access rights defined. This is done recursive so the access rights can be set on a parent folder. For convenience, I have placed all the code in the same controller. More about what the controller does in the code comments below.

using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Security;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Security;
using Umbraco.Web;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;

namespace UmbracoTests.Controllers
{
    public class MediaController : Controller
    {
        private const string MediaRootFolder = "media";
        private const string AccessRightsPropertyAlias = "accessRights";

        public MediaController()
        {
            // We´re using the UmbracoHelper to access check so we need to
            // ensure Umbraco context.
            UmbracoContext.EnsureContext(
              new HttpContextWrapper(System.Web.HttpContext.Current),
              ApplicationContext.Current,
              new WebSecurity(
                  new HttpContextWrapper(System.Web.HttpContext.Current),
                  ApplicationContext.Current),
              UmbracoConfig.For.UmbracoSettings(),
              UrlProviderResolver.Current.Providers,
              true);
        }

        public ActionResult Index(string id, string file)
        {
            // Construct the media file path
            var mediaPath = $"/{MediaRootFolder}/{id}/{file}";

            // No access control for image files, because we don´t want a
            // performance dip accessing images on the website in general.
            // If you need to protect images put them, for example, into a
            // zip-file.
            if (IsImage(mediaPath))
            {
                return TransferFile(mediaPath);
            }

            // Get media from database. Database is costly but seems to be 
            // the only way to get media by path.
            var mediaService = ApplicationContext.Current.Services.MediaService;
            var media = mediaService.GetMediaByPath(mediaPath);

            // Handle media not found
            if (media == null)
            {
                return HttpNotFound();
            }

            // Get media from cache using the id
            var Umbraco = new UmbracoHelper(UmbracoContext.Current);
            var typedMedia = Umbraco.TypedMedia(media.Id);

            // If media has access rights; checked recursive (means a parent  folder 
            // could have access rights defined).
            var mediaAccessRights = typedMedia.GetPropertyValue<string>(AccessRightsPropertyAlias, true);
            if (!string.IsNullOrWhiteSpace(mediaAccessRights)

                // If logged into the backoffice, it will bypass access check.
                // Note: I don´t know if there´s a better way to do this check, 
                //       but it was the best working solution at this moment.
                && HttpContext.GetUmbracoAuthTicket() == null)
            {
                // The member group picker property in Umbraco store values as comma separated.
                var rightsArr = mediaAccessRights?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                if (!rightsArr.Any(x => Roles.IsUserInRole(x)))
                {
                    return new HttpStatusCodeResult(HttpStatusCode.Forbidden);
                }
            }

            return TransferFile(mediaPath);
        }

        /// <summary>
        /// Holds the image file extensions.
        /// </summary>
        private static readonly string[] ImageFileExtensions =  
        {
            ".jpg", ".jpeg", ".png", ".gif", ".tif", ".tiff", ".svg"
        };

        /// <summary>
        /// Returns true if is image from supplied path.
        /// </summary>
        private bool IsImage(string path)
        {
            var extension = Path.GetExtension(path)?.ToLower();
            if (!string.IsNullOrEmpty(extension))
            {
                return ImageFileExtensions.Contains(extension);
            }
            return false;
        }

        /// <summary>
        /// Opens the media as filestream using the current virtual file system
        /// and returns it to the client. This will also works if you´re using 
        /// the Azure Blob Storage provider for Umbraco.
        /// </summary>
        private ActionResult TransferFile(string path)
        {
            var fileStream = VirtualPathProvider.OpenFile(path);
            return File(fileStream, MimeMapping.GetMimeMapping(path));
        }
    }
}

Protect a media folder

Go to the Media section in the Umbraco Backoffice and create a folder. In the Security tab add the AccessToFiles member group to the Access Rights property.

 

Now put some files into that folder and try to access it in the browser without being logged into the Umbraco Backoffice. You should see a HTTP Error 403 – Forbidden – as the one below.

 

Now logging in as the member we´ve created earlier should reveal the PDF document.

 

Tip: If you don´t have a member login for your site you could login as the new member created running the snippet below:

if (Membership.ValidateUser("secretmember", "{YOUR_PASSWORD}"))
{
    FormsAuthentication.SetAuthCookie("secretmember", true);
}

An alternative way

As a alternative solution you could use the IIS Rewrite module to point all media (except images) to a generic handler where access check will be done instead. Here follows a IIS rewrite rule to do so.

<rule name="File redirect" stopProcessing="true">
   <match url="^(media/([0-9]+)/.*\.(?!jpg|jpeg|png|gif|svg|tif|tiff)[^.]+)$" />
   <action type="Rewrite" url="/MyAccessCheckHandler.ashx?url={R:1}" appendQueryString="false" />
</rule>

That was pretty much it. Routing in ASP.NET MVC gives us great tools to add new functionality.