Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Nathan 11 posts 101 karma points c-trib
    Sep 19, 2023 @ 22:35
    Nathan
    0

    Accessing members from UmbracoApiController to secure media files in v8

    Hello everyone,

    I’m trying to secure media files on my Umbraco v8.18.8 website based on, among other factors, whether or not a member is currently logged in. The method I'm using to achieve this is roughly based on this blog post. I have a handler class that successfully intercepts all requests to /media/*:

    public class MediaHandler : HttpTaskAsyncHandler
    {
        public MediaHandler() { }
    
        public override bool IsReusable
        {
            get { return false; }
        }
    
        public override async Task ProcessRequestAsync(HttpContext context)
        {
            List<KeyValuePair<string, object>> parameters = new List<KeyValuePair<string, object>>();
            parameters.Add(new KeyValuePair<string, object>("mediaPath", context.Request.Path));
            string scheme = context.Request.Url.Scheme;
            string authority = context.Request.Url.Authority;
    
            string url = BuildApiUrl(
                domainAddress: scheme + "://" + authority,
                apiLocation: "Umbraco/Api/",
                controllerName: "ProtectedMediaApi",
                methodName: "IsAllowed",
                parameters: parameters);
    
            HttpResponseMessage isAllowedResponse = await GetResultFromApi(url);
    
            if (!isAllowedResponse.IsSuccessStatusCode)
            {
                context.Response.StatusDescription = isAllowedResponse.ReasonPhrase;
                context.Response.StatusCode = (int)isAllowedResponse.StatusCode;
                return;
            }
    
            bool isAllowed = JsonConvert.DeserializeObject<bool>(await isAllowedResponse.Content.ReadAsStringAsync());
    
            if (isAllowed)
            {
                string requestedFile = context.Server.MapPath(context.Request.Path);
                SendContentTypeAndFile(context, requestedFile);
            }
            else
            {
                context.Response.StatusDescription = "Forbidden";
                context.Response.StatusCode = 403;
            }
        }
    
        private string BuildApiUrl(string domainAddress, string controllerName, string methodName, List<KeyValuePair<string, object>> parameters, string apiLocation)
        {
            StringBuilder url = new StringBuilder();
    
            url.Append($"{domainAddress}/{apiLocation}{controllerName}/{methodName}");
    
            if (parameters != null && parameters.Count > 0)
            {
                int parameterCount = parameters.Count;
                for (int i = 0; i < parameterCount; i++)
                {
                    url.Append(i == 0 ? "?" : "&");
                    url.Append($"{parameters[i].Key}={parameters[i].Value.ToString()}");
                }
            }
    
            return url.ToString();
        }
    
        private async Task<HttpResponseMessage> GetResultFromApi(string url)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                HttpResponseMessage response = await httpClient.GetAsync(url);
                return response;
            }
        }
    
        private HttpContext SendContentTypeAndFile(HttpContext context, String strFile)
        {
            context.Response.ContentType = MimeMapping.GetMimeMapping(strFile);
            context.Response.TransmitFile(strFile);
            context.Response.End();
            return context;
        }
    }
    

    The handler is designated in my Web.config file like so: <add name="MediaHandler" path="/media/*" verb="*" type="MembersPortalV8.Handlers.MediaHandler"/>

    The API being called in MediaHandler is defined in a separate class, ProtectedMediaApiController, which inherits from UmbracoApiController as such:

    public class ProtectedMediaApiController : UmbracoApiController
    {
        private readonly IMediaService _mediaService;
    
        public ProtectedMediaApiController(
            IGlobalSettings globalSettings,
            IUmbracoContextAccessor umbracoContextAccessor,
            ISqlContext sqlContext,
            ServiceContext services,
            AppCaches appCaches,
            IProfilingLogger logger,
            IRuntimeState runtimeState,
            UmbracoHelper umbracoHelper,
            UmbracoMapper umbracoMapper,
            IMediaService mediaService
        ) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper, umbracoMapper)
        {
            _mediaService = mediaService;
        }
    
        [HttpGet]
        public bool IsAllowed(string mediaPath)
        {
            if (string.IsNullOrEmpty(mediaPath))
            {
                return false;
            }
    
            IMedia media = _mediaService.GetMediaByPath(mediaPath);
    
            if (media == null)
            {
                return false;
            }
    
            IPublishedContent typedMedia = Umbraco.Media(media.Id);
            IEnumerable<IPublishedContent> ancestorList = typedMedia.Ancestors();
    
            bool isPublic = false;
    
            foreach (IPublishedContent ancestor in ancestorList)
            {
                if (ancestor.Value<bool>("isPublic"))
                {
                    isPublic = true;
                    break;
                }
            }
    
            bool isLoggedIn = Members.IsLoggedIn();
    
            if (!isPublic && !isLoggedIn)
            {
                return false;
            }
    
            return true;
        }
    }
    

    Accessing mediaService through DI works as expected, and I am able to check properties on the specified media file. For some reason, however, I cannot get the current member - every approach I have tried so far either returns false or null. Some examples:

    • Members.IsLoggedIn() (as shown above) - always returns false, even if a member is logged in. Members should be accessible here since it is a property of UmbracoApiControllerBase, which UmbracoApiController inherits from (source).
    • _membershipHelper.IsLoggedIn() (where a new MembershipHelper is passed in through DI) - always returns false.
    • _umbracoHelper.MemberIsLoggedOn() (where UmbracoHelper is retrieved through DI) - always returns false.

    This forum post discusses the same issue. The OP claims they passed the current member's username as a parameter to their API method; however, the class my API method is being called from inherits HttpTaskAsyncHandler, which, as far as I know, would not allow me to DI a MembershipHelper.

    The forum post also mentions using the MemberAuthorize attribute - interestingly, this does work, and logged-out members are not able to call the API method, which means that this functionality is at least possible. Unfortunately, I also need to check members' roles to determine if they can access a given media file. Since other member-related methods do not work (_membershipHelper.GetCurrentLoginStatus() is always null, _membershipHelper.GetCurrentMember() is always null), this would not be a sufficient workaround.

    Some other options I have tried include:

    • Using ApiController instead of UmbracoApiController or attempting to inject Umbraco dependencies into MediaHandler (does not give access to Umbraco services - tried some options discussed in this thread but could not figure out how to get it working for my purpose)
    • Inheriting UmbracoHttpHandler or UmbracoAuthorizedHttpHandler instead of IHttpHandler or HttpTaskAsyncHandler (seemingly not possible, see this github issue)

    According to the github issue I mentioned earlier, it might be due to Umbraco interpreting the request as client-side and therefore not providing an UmbracoContext - unfortunately I cannot change the extension to force a server-side request as suggested, as my handler needs to run for all media files of any extension. Another suggestion was to use MVC route hijacking, which may be the proper way to go about doing this, but I was having some other issues with that (will create a separate thread for it!)

    I am still relatively new to Umbraco so there is most likely something obvious I'm missing here. I will post an update to this thread if I find anything, but in the mean time, any help/advice/suggestions would be greatly appreciated!

  • Marc Goodson 2155 posts 14408 karma points MVP 9x c-trib
    Sep 20, 2023 @ 08:57
    Marc Goodson
    0

    Hi Nathan

    There is an open source package called Our.Shield.MediaProtection that does I think something similar to what you are attempting to achieve.

    If you can't use this package and need to build you own, you might be able to glean some insights into how they have implemented this, for example:

    https://github.com/JcRichards1991/Our.Shield/blob/master-umbraco-v8/src/Our.Shield.MediaProtection/Models/MediaProtectionApp.cs

    has the code in that checks the current member etc

    regards

    Marc

  • Nathan 11 posts 101 karma points c-trib
    Sep 20, 2023 @ 15:52
    Nathan
    0

    Hi Marc,

    Thanks for the quick response! Unfortunately, I don't think I can use this package as-is, since it's currently only compatible with Umbraco v7 (see here).

    Looking at the source code, it seems as though they're using Context.User.Identity.IsAuthenticated to check if a member is logged in. Attempting to do the same in my MediaHandler by accessing the HttpContext passed in as a parameter to ProcessRequestAsync (context.User.Identity.IsAuthenticated) again always returns false regardless if a member is logged in. I can't figure out exactly how they've implemented the request interception, but considering they are able to get the proper authentication data from the context, I don't think I'm taking the right approach here.

  • Marc Goodson 2155 posts 14408 karma points MVP 9x c-trib
    Sep 21, 2023 @ 09:18
    Marc Goodson
    0

    Ahh Sorry Nathan

    I just looked at the packages.config and the default branch made me think there was a V8 version :-(

    https://github.com/JcRichards1991/Our.Shield/blob/master-umbraco-v8/src/Our.Shield.MediaProtection/packages.config

    so hoped it would have something useful in there for you.

    regards

    marc

  • Nathan 11 posts 101 karma points c-trib
    Sep 22, 2023 @ 13:52
    Nathan
    0

    No worries! I really appreciate you taking the time to respond regardless. It does look like they have a v8 version of the package in development, but it's still WIP. I might be able to just fork the package and finish it, but that sounds like a lot of work for something I'm hoping to do in a relatively simple way.

  • Huw Reddick 1929 posts 6697 karma points MVP 2x c-trib
    Sep 21, 2023 @ 07:40
    Huw Reddick
    1

    Hi Nathan,

    You can get the username of the logged in user in your handler and pass it to the UmbracoApi

            var user = new HttpContextWrapper(HttpContext.Current).User;
    
            List<KeyValuePair<string, object>> parameters = new List<KeyValuePair<string, object>>
            {
                new KeyValuePair<string, object>("username", user.Identity.Name),
                new KeyValuePair<string, object>("mediaPath", context.Request.FilePath)
            };
    
            ApiHelper apiHelper = new ApiHelper();
            var port = (context.Request.Url.Port != 443) ? ":" + context.Request.Url.Port : "";
            string url = apiHelper.BuildApiUrl(
                domainAddress: "https://" + context.Request.Url.Host + port,
                apiLocation: "Umbraco/Api/",
                controllerName: "ProtectedMediaApi",
                methodName: "IsAllowed",
                parameters: parameters);
    
  • Nathan 11 posts 101 karma points c-trib
    Sep 22, 2023 @ 14:23
    Nathan
    0

    Hi Huw,

    Thanks for the response! Unfortunately, new HttpContextWrapper(HttpContext.Current).User always returns null when called in my handler. My suspicion is that because it's a media link, the HttpContext doesn't contain any data that pertains to Umbraco, but I haven't been able to find a workaround.

  • Huw Reddick 1929 posts 6697 karma points MVP 2x c-trib
    Sep 23, 2023 @ 07:32
    Huw Reddick
    0

    Hi Nathan,

    The Handler does not know anthing about umbraco, however that should not be why the User is null the new HttpContextWrapper(HttpContext.Current).User is also not directly related to Umbraco other than it should be set when a member logs in, so sounds like you handler may be running to early in the request pipeline.

    try adding runAllManagedModulesForAllRequests="True" to the modules element in web.config

    You could also try inheriting SessionState in your handler

    CustomHandler: IHttpHandler, IReadOnlySessionState
    
  • Nathan 11 posts 101 karma points c-trib
    Sep 25, 2023 @ 16:09
    Nathan
    0

    Hi Huw,

    That makes sense - I already had runAllManagedModulesForAllRequests="True" set in my web.config, but I don't have a custom module - would it be necessary for me to implement one? I did a bit of research but I couldn't find any guidance on what exactly it would look like for this scenario. Unfortunately inheriting IReadOnlySessionState didn't change anything either, unless there's something I need to add to my ProcessRequestAsync method, but I assume not since IReadOnlySessionState is meant to be a marker interface.

  • Huw Reddick 1929 posts 6697 karma points MVP 2x c-trib
    Sep 25, 2023 @ 16:21
    Huw Reddick
    0

    maybe it is not working with HttpTaskAsyncHandler mine Implements IHttpHandler

  • Nathan 11 posts 101 karma points c-trib
    Sep 25, 2023 @ 20:42
    Nathan
    0

    I did have it my handler implementing IHttpHandler earlier, before I wanted an async ProcessRequest method, but I tried again and still no luck... I'm pretty much completely out of ideas here, I did try manually getting the cookie from the request (which does work!) but I'm not sure how I would be able to check a member's groups with it.

    For now I'm going to focus on upgrading my site to v10 and implementing this functionality there, but if anyone has any more suggestions I'll definitely come back to this!

  • Huw Reddick 1929 posts 6697 karma points MVP 2x c-trib
    Sep 26, 2023 @ 08:02
    Huw Reddick
    0

    Just a thought, as I remember having to do this once before.

    in the modules section of webconfig, try adding this at the end

      <remove name="FormsAuthentication" />
      <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />
    
  • Nathan 11 posts 101 karma points c-trib
    Sep 26, 2023 @ 14:59
    Nathan
    0

    Still no luck, unfortunately. Thank you for all of the suggestions, though!

  • Huw Reddick 1929 posts 6697 karma points MVP 2x c-trib
    Sep 26, 2023 @ 17:27
    Huw Reddick
    0

    Sorry it wasn't helpful, just seems to work for me.

  • Huw Reddick 1929 posts 6697 karma points MVP 2x c-trib
    Sep 26, 2023 @ 17:29
    Huw Reddick
    0

    For V10 you will require a middleware class registered, I have an example of that if you need one.

  • Nathan 11 posts 101 karma points c-trib
    Sep 26, 2023 @ 17:52
    Nathan
    0

    No worries! It's a very strange issue. Sure, if you wouldn't mind posting your example that would be super helpful! Thanks again!

Please Sign in or register to post replies

Write your reply to:

Draft