asp.net-mvcasp.net-mvc-3asp.net-mvc-routinghttpmodulemvcroutehandler

Routing static files with ASP.net MVC from a different directory without IIS changes and with authentication


Lets stay I store content for a single user of my application at:

Those files may be something like:

The end user should be presented with a "friendly" url.

http:// domain.com/files/{GUID}/bunny.jpg

That url somehow must pass through a controller or httpmodule or thingIdontknow to be authorized to view that file. These permissions may change day to day so files need to be checked for permissions often.

From what I've been reading this is entirely possible but I'm not sure what to code up next or if anybody has any insight here. HttpModule or Controller? I'm confused as to what needs to happen.


Solution

  • That url somehow must pass through a controller or httpmodule or thingIdontknow to be authorized to view that file. These permissions may change day to day so files need to be checked for permissions often.

    The thing you don't know has a name. It's called an authorization action filter.

    First let's suppose that you have registered a custom route for serving those files:

    routes.MapRoute(
        "MyImagesRoute",
        "files/{id}/{name}",
        new { controller = "Files", action = "Index" }
    
        // TODO: you could constrain the id parameter to be a GUID.
        // Just Google for a Regex that will match a GUID pattern and put here
        // as route constraint
    );
    

    and then of course a corresponding controller to serve them:

    public class FilesController: Controller
    {
        public ActionResult Index(Guid guid, string name)
        {
            var path = @"C:\files";
            var file = Path.Combine(path, guid.ToString(), name);
            file = Path.GetFullPath(file);
            if (!file.StartsWith(path))
            {
                // someone tried to be smart and send
                // files/{Guid}/..\..\creditcard.pdf as parameter
                throw new HttpException(403, "Forbidden");
            }
    
            // TODO: adjust the mime type based on the extension
            return File(file, "image/png");
        }
    }
    

    Unfortunately at this stage there's nothing preventing a user ALPHA from requesting the file of user BETA, right? That's the scenario you would like to handle, aren't you?

    So let's write a custom Authorize attribute to protect this controller action:

    public class MyAuthorizeAttribute: AuthorizeAttribute
    {
        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            var authorized = base.AuthorizeCore(httpContext);
            if (!authorized)
            {
                // The user is not authenticated or doesn't have 
                // permissions to access this controller action
                return false;
            }
    
            // at this stage we know that there's some user authenticated
    
            // Let's get the Guid now from our route:
            var routeData = httpContext.Request.RequestContext.RouteData;
    
            var id = routeData.Values["id"] as string;
            Guid guid;
            if (!Guid.TryParse(id, out guid))
            {
                // invalid Guid => no need to continue any further, just deny access
                return false;
            }
    
            // Now we've got the GUID that this user is requesting
    
            // Let's see who this user is:
            string username = httpContext.User.Identity.Name;
    
            // and finally ensure that this user
            // is actually the owner of the folder
            return IsAuthorized(username, guid);
        }
    
        private bool IsAuthorized(string username, Guid guid)
        {
            // You know what to do here: hit your data store to verify
            // that the currently authenticated username is actually
            // the owner of this GUID
            throw new NotImplementedException();
        }
    }
    

    and then let's decorate our controller action with this authorization attribute:

    public class FilesController: Controller
    {
        [MyAuthorize]
        public ActionResult Index(Guid guid, string name)
        {
            // at this stage we know that the currently authenticated user
            // is authorized to access the file.
    
            var path = @"C:\files";
            var file = Path.Combine(path, guid.ToString(), name);
            file = Path.GetFullPath(file);
            if (!file.StartsWith(path))
            {
                // someone tried to be smart and send
                // files/{Guid}/..\..\creditcard.pdf as parameter
                throw new HttpException(403, "Forbidden");
            }
    
            var file = Path.Combine(@"c:\files", guid.ToString(), name);
            // TODO: adjust the mime type based on the extension
            return File(file, "image/png");
        }
    }