What is this post about?

This post shows you how to protect media in Umbraco. It works on the idea that you can put the media you want to protect in a parent folder, perhaps called 'Protected'. In that you can have your files and folders which are not public. But you can also have any other public media files and folders which can be seen by all if they outside of this folder.

Where to start?

First of all, we need to think about what is required to achieve this. 

We will need:

  • A Media Handler to check if it is allowed to serve the file and then serve it if it is.
  • An API Controller with a method on it which checks if the media item is public or protected and whether the visitor is a logged in member or not.
  • An API Helper to make it easier to call the API
  • A web.config file in the media folder to say to use the Media Handler for serving files from this folder.

The Media Handler

Let's start with a media handler which serves the files and calls an API method to see if it is allowed to serve this item. I put this in the web project in a Handlers folder.

using CodeShare.Library.Helpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
namespace CodeShare.Web.Handlers { public class MediaHandler : IHttpHandler {
public MediaHandler() { }
public bool IsReusable { get { return false; } }
public void ProcessRequest(HttpContext context) { var user = new HttpContextWrapper(HttpContext.Current).User;
List<KeyValuePair<string, object>> parameters = new List<KeyValuePair<string, object>>(); parameters.Add(new KeyValuePair<string, object>("username", user.Identity.Name)); parameters.Add(new KeyValuePair<string, object>("mediaPath", context.Request.FilePath));
ApiHelper apiHelper = new ApiHelper();
string url = apiHelper.BuildApiUrl( domainAddress: "http://www.example.com", apiLocation: "Umbraco/Api/", controllerName: "ProtectedMediaApi", methodName: "IsAllowed", parameters: parameters);
bool isAllowed = apiHelper.GetResultFromApi(url);
if (isAllowed) { string requestedFile = context.Server.MapPath(context.Request.FilePath); SendContentTypeAndFile(context, requestedFile); } else { context.Response.Status = "403 Forbidden"; context.Response.StatusCode = 403; } }
HttpContext SendContentTypeAndFile(HttpContext context, String strFile) { context.Response.ContentType = GetContentType(strFile); context.Response.TransmitFile(strFile); context.Response.End(); return context; }
public string GetContentType(string filename) { string res = null; FileInfo fileinfo = new FileInfo(filename); if (fileinfo.Exists) { switch (fileinfo.Extension.Remove(0, 1).ToLower()) { case "pdf": { res = "application/pdf"; break; } case "doc": { res = "application/msword"; break; } case "docx": { res = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; break; } case "xls": { res = "application/vnd.ms-excel"; break; } case "xlsx": { res = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; break; } case "jpg": { res = "image/jpeg"; break; } } return res; } return null; }
} }

As you can see it has these methods on it. 

  • IsReusable
  • ProcessRequest
  • SendContentTypeAndFile
  • GetContentType

The first 2 methods are required by the interface it inherits from IHttpHandler

The second 2 are used for sending the file once it has been established it is allowed to do.

This method checks the file extension and adds the right encoding type for it. You can add more for different file types.

IsReusable

This is just set to return false at the moment. You will get different answers depending on who you speak to about this. But it is safe to set it to false.

ProcessRequest

This is the main method on this handler. It is this method which checks if it is allowed to serve the media and then acts on the result of that check.

This line gets the User from the current HttpContext. We need this when checking if the user is authorised to see this media item.

var user = new HttpContextWrapper(HttpContext.Current).User;

This next part is where we call an api method to find out if the user is allowed to see this media item. Instead of hard coding the domain address, you should store it in you appsettings.

List<KeyValuePair<string, object>> parameters = new List<KeyValuePair<string, object>>();
parameters.Add(new KeyValuePair<string, object>("username", user.Identity.Name));
parameters.Add(new KeyValuePair<string, object>("mediaPath", context.Request.FilePath));
ApiHelper apiHelper = new ApiHelper();
string url = apiHelper.BuildApiUrl( domainAddress: "http://www.example.com", apiLocation: "Umbraco/Api/", controllerName: "ProtectedMediaApi", methodName: "IsAllowed", parameters: parameters);
bool isAllowed = apiHelper.GetResultFromApi(url);

Api Helper

See this blog post for the ApiHelper code.

GetContentType

This method gets the content type of the file by checking the file extension. You can add more extensions and content types to this method if you want.

SendContentTypeAndFile

This calls gets the content type by calling the GetContentType method and then it transmits the file to the user.

The API Controller

This protected media API Controller uses the username of the member and the path of the media file. I put this in the web project, controllers folder, then in a new folder called ApiControllers. Notice the class inherits from UmbracoApiController.


It checks if the media item they are trying to view is within the media folder which has protected media in it or not. Instead of hardcoding the folder id in, you should store it in your app settings.


Please note, this code only works in v7.5+ of Umbraco because of the GetMediaByPath method which was only fixed in v7.5.

using System.Linq;
using System.Web.Http;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.WebApi;

namespace CodeShare.Web.Controllers.ApiControllers
{
    public class ProtectedMediaApiController : UmbracoApiController
    {
        private IMemberService _memberService => ApplicationContext.Services.MemberService;
        private IMediaService _mediaService => ApplicationContext.Services.MediaService;        

        [HttpGet]
        public bool IsAllowed(string username, string mediaPath)
        {
            int protectedMediaFolderId = 1150;
        
            bool isAllowed = false;
            IMedia mediaItem = _mediaService.GetMediaByPath(mediaPath);
            bool isProtected = mediaItem.Path.Split(',').ToList().Contains(protectedMediaFolderId.ToString());
            if(isProtected)
            {
                IMember member = _memberService.GetByUsername(username);
                if (member != null && member.Id > 0)
                {
                    //You could add rules in here to check the member is in a certain role.
                    isAllowed = true;
                }
            }
            else
            {
                isAllowed = true;
            }
            
            return isAllowed;
        }
    }
}

The web.config file in the media folder.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<handlers>
<clear />
<add name="DOCX" path="*.docx" verb="*" type="CodeShare.Web.Handlers.MediaHandler" />
<add name="XLSX" path="*.xlsx" verb="*" type="CodeShare.Web.Handlers.MediaHandler" />
<add name="DOC" path="*.doc" verb="*" type="CodeShare.Web.Handlers.MediaHandler" />
<add name="XLS" path="*.xls" verb="*" type="CodeShare.Web.Handlers.MediaHandler" />
<add name="PDF" path="*.pdf" verb="*" type="CodeShare.Web.Handlers.MediaHandler" />
<add name="JPG" path="*.jpg" verb="*" type="CodeShare.Web.Handlers.MediaHandler" />
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule,DefaultDocumentModule,DirectoryListingModule" resourceType="Either" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>

That's it

You should be able to build and run. Don't forget to create a folder to put your protected media in, and use the id of that folder in the API Controller.

Now with everything in place, when a user tries to see a protected media item, if they don't have permission, they will get a '403 Forbidden' error.

I will do another about how to display custom error messages in future.

Want to thank me?

If I've helped you out and you want to thank me, why not buy me a coffee?

Paul Seal

A .NET Web Developer from Derby (UK) who specialises in building Content Management System (CMS) websites using MVC with Umbraco as a framework. Paul is passionate about web development and programming as a whole. Apart from when he's with his wife and son, if he's not writing code, he's thinking about it or listening to a podcast about it.

Web: codeshare.co.uk

Comments

Share Top