Copied to clipboard

Flag this post as spam?

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


  • Jamie Attwood 208 posts 504 karma points c-trib
    May 10, 2022 @ 19:13
    Jamie Attwood
    0

    Intercept All Media Requests

    Using Umbraco 9 I need to 1) Intercept all media requests 2) Evaluate if a member is logged in 3) If so, pass along the media requested, if not send a 401 status code.

    So far I have attempted creating a middleware to do this but I am having an issue in that I can't accurately detect media requests and also, it looks like I can't inject the member manager as I think it's too early in the lifecycle of the app. Any thoughts on how to achieve this would be great.

    My Middleware class:

    using Microsoft.AspNetCore.Http;
    using System.Threading.Tasks;
    using Umbraco.Cms.Core.Security;
    
    namespace Ninepoint.Core.Middlewares
    {
        public class AuthorizedMediaAccess
        {
            private readonly RequestDelegate _next;
    
            public AuthorizedMediaAccess(RequestDelegate next)
            {
                _next = next;
            }
    
            public async Task  InvokeAsync(HttpContext context, IMemberManager memberManager) 
            {
                //If its not media, move on...
                if (!context.Request.Path.StartsWithSegments("/media"))
                {
                    await _next(context);
                }
    
                // Don't return a 401 response if the user is already authenticated.
                //if (context.User.Identities.Any(IdentityExtensions => IdentityExtensions.IsAuthenticated))
                if (memberManager.IsLoggedIn())
                {
                    await _next(context);
                }
    
                // Stop processing the request and return a 401 response.
                context.Response.StatusCode = 401;
            }
        }
    }
    

    It is being called in startup.cs here in the "WithMiddleWare" method:

      app.UseUmbraco()
                    .WithMiddleware(u =>
                    {
                        u.UseBackOffice();
                        u.UseWebsite();
                        u.AppBuilder.UseMiddleware<AuthorizedMediaAccess>();
                    })
    
  • Andrea Lovo 3 posts 73 karma points
    May 11, 2022 @ 14:17
    Andrea Lovo
    0

    1,2) try this:

    StatupFilter

       public class StartupFilter : IStartupFilter
        {
            public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) => app =>
            {
                app.UseMiddleware<AuthorizedMediaAccess>();
                next(app);
            };
        }
    

    Startup.cs

     public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IStartupFilter, StartupFilter>();
    
            services.AddUmbraco(_env, _config)
                .AddBackOffice()
                .AddWebsite()
                .AddComposers()
                .Build();
        }
    

    Middleware(InvokeAsync)

     public async Task InvokeAsync(HttpContext context, IMemberManager memberManager)
        {
            //If its not media, move on...
    
            if (!context.Request.Path.StartsWithSegments("/media"))
            {
                await _next(context);
            }
            else
            {
                if (context.User.Identity.IsAuthenticated)
                {
                    await _next(context);
                }
                else
                {
                    context.Response.StatusCode = 401;
                }
            }
    
        }
    

    And remember to clear the cache!

  • Jamie Attwood 208 posts 504 karma points c-trib
    May 11, 2022 @ 15:22
    Jamie Attwood
    0

    "Clear the cache" YES! that was most of the problem with my first solution. But, Filters to the rescue. Thank you for this suggestion. It's mostly working now. The only issue remailing is that context.User.Identity is always null. Any idea on how to evaluate user this early on in the program's lifecycle?

  • David Zweben 268 posts 754 karma points
    May 11, 2022 @ 17:18
    David Zweben
    0

    I am trying to do this exact same thing right now, so I’m watching this thread with great interest.

  • Jamie Attwood 208 posts 504 karma points c-trib
    May 11, 2022 @ 17:43
    Jamie Attwood
    0

    That issue I think is likely that the startup filters are fired long before the umbraco middleware is fired and as a result we can't get the user. The above works from a filtering standpoint, however filtering on "/media" does not work from within the UseUmbraco(). AppBuilder.UseMiddleware for some reason. The media path is removed out the request pipeline at some point. This is so frustrating. So much obscured filtering and middlewares. I am on day two now... lol.

  • David Zweben 268 posts 754 karma points
    May 11, 2022 @ 17:52
    David Zweben
    0

    I actually have a really hacky approach working for protected images that involves passing the media ID to a web API endpoint, and that endpoint querying the image file path, loading the image into a PhysicalFile, and returning an actual image file as the response from the API. This doesn't protect the images at their real URLs, but provides a way to serve them with protection without revealing the unprotected URL. It's security-through-obscurity, but it could be sufficient in some cases.

    The problem I ran into is that I need to also serve thumbnails that are processed through ImageSharp, and that has basically proven incompatible with the approach because I can't figure out how to get the physical file path of the cached ImageSharp thumbnails.

  • David Zweben 268 posts 754 karma points
    May 11, 2022 @ 18:06
    David Zweben
    0

    Just throwing some more thoughts out there to see if I can help:

    One approach might be to implement a custom FileSystemProvider for ImageSharp that does access checking and then replicates the functionality of the standard PhysicalFileSystemProvider only when access is allowed, or maybe even calls into that existing implementation.

    Relevant discussions:

    https://our.umbraco.com/forum/umbraco-9/106879-umbraco-9-and-media-folder-location

    https://github.com/umbraco/Umbraco-CMS/issues/11580#issuecomment-991716300

  • Jamie Attwood 208 posts 504 karma points c-trib
    May 11, 2022 @ 20:33
    Jamie Attwood
    101

    Ok I have finally come up with a somewhat decent solution... feel free to chime in here if you want. We can't use filters for this as that occurs too early in the pipeline, but we can use a custom middleware in startup.cs Configure() before app.UseUmbraco() get's its hands all over our media endpoints. The only redundant part about this is if we want to authenticate the user we need to call app.UseAuthentication(); prior to our new middleware. Note that in my case, just authenticating the user is good enough. You might be able to add app.UseAuthorization(); to get into roles etc. Anyway here it is and it should not interfere with any of the authentication logic or ImageSharp, etc.

    The middleware (CustomMediaAuthentication.cs)

    using Microsoft.AspNetCore.Http;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace Ninepoint.Core.Middlewares
    {
        public class CustomMediaAuthentication
        {
            private readonly RequestDelegate _next;
    
            public CustomMediaAuthentication(RequestDelegate next)
            {
                _next = next;
            }
    
            public async Task InvokeAsync(HttpContext context)
            {
                //If its not media, move on...
                if (!context.Request.Path.StartsWithSegments("/media"))
                {
                    await _next(context);
                    return;
                }
    
                //Authenticate user
                if (context.User.Identities.Any(IdentityExtensions => IdentityExtensions.IsAuthenticated))
                {
                    await _next(context);
                    return;
                }
    
                // Stop processing the request and return a 401 response.
                context.Response.StatusCode = 401;
                await Task.FromResult(0);
                return;
            }
        }
    }
    

    The extension class (CustomMediaAuthenticationExtension.cs)

    using Microsoft.AspNetCore.Builder;
    
    namespace Ninepoint.Core.Middlewares
    {
        public static class IApplicationBuilderExtention
        {
            public static IApplicationBuilder UseCustomMediaAuthentication(this IApplicationBuilder app)
            {
                return app.UseMiddleware<CustomMediaAuthentication>();
            }
        }
    }
    

    And the call in Startup.cs:

        //UseCustomMediaAuthentication() relies on UseAuthentication()
        //Include before app.UseUmbraco()
        app.UseAuthentication();
        app.UseCustomMediaAuthentication();
    
  • Jamie Attwood 208 posts 504 karma points c-trib
    May 12, 2022 @ 00:06
    Jamie Attwood
    0

    And after a little more experimentation and nudging from @bill-the-duck whos post here has since disappeared, after your call to app.UseAuthentication(); you can then test member related access via either the memberService (which you can inject via the constructor) and also you can method inject the IMemberManager if you want more performant results, Something like this:

    public class CustomMediaAuthentication
        {
            private readonly RequestDelegate _next;
            private readonly IMemberService _memberService;
    
            public CustomMediaAuthentication(RequestDelegate next, IMemberService memberService)
            {
                _next = next;
                _memberService = memberService;
            }
    
            public async Task InvokeAsync(HttpContext context, IMemberManager memberManager)
            {
                //If its not media, move on...
                if (!context.Request.Path.StartsWithSegments("/media"))
                {
                    await _next(context);
                    return;
                }
    
    
                if (memberManager.IsLoggedIn()) //Authenticate user via memberManager through method DI
                {
                    var member = _memberService.GetByUsername(context.User.Identity.Name); //Get member from _memberService
                    var memberManagerMember = memberManager.GetCurrentMemberAsync(); //Get member from memberManager
                    //Check out member permissions and do stuff....
                    await _next(context);
                    return;
                }
    
                // Stop processing the request and return a 401 response.
                context.Response.StatusCode = 401;
                await Task.FromResult(0);
                return;
            }
        }
    

    Cheers!

    Jamie

  • David Zweben 268 posts 754 karma points
    May 12, 2022 @ 12:24
    David Zweben
    0

    This is great! Worked perfectly.

  • David Zweben 268 posts 754 karma points
    May 12, 2022 @ 20:30
    David Zweben
    0

    Hmm, I ran into an issue, maybe one of you know of a way to address it.

    The media protection is working fine on the website's front-end, but in the Umbraco backoffice, if I select an image in the RTE image picker and save, the image URL gets cleared out after save. I think basically the media protection code isn't working correctly from the context of the backoffice, and it's causing things to break.

    I think all I need is a consistent way to identify if a media request is being made by the backoffice so I can put in an exception. Unfortunately, I didn't see anything obvious that would allow me to do that. Any ideas?

    The only other thing I can think of is to perhaps check for a logged in User in addition to a logged in Member, and use that to bypass some of the logic.

  • Jamie Attwood 208 posts 504 karma points c-trib
    May 12, 2022 @ 20:56
    Jamie Attwood
    0

    Crap. I did some very high level testing only. I will look into that a little deeper.

    Have you tried something like this? (not tested)

    public async Task InvokeAsync(HttpContext context, IMemberManager memberManager, IBackOfficeSecurityAccessor backOfficeSecurityAccessor )
            {
                //If its not media, move on...
                if (!context.Request.Path.StartsWithSegments("/media"))
                {
                    await _next(context);
                    return;
                }
    
    
                if (memberManager.IsLoggedIn() || backOfficeSecurityAccessor.BackOfficeSecurity.IsAuthenticated()) 
                {
    
                    await _next(context);
                    return;
                }
    
                // Stop processing the request and return a 401 response.
                context.Response.StatusCode = 401;
                await Task.FromResult(0);
                return;
            }
    
  • David Zweben 268 posts 754 karma points
    May 12, 2022 @ 21:03
    David Zweben
    0

    Unfortunately, backOfficeSecurityAccessor.BackOfficeSecurity.IsAuthenticated() is returning False even from a backoffice context inside this method.

    context.User.GetUmbracoIdentity(); is also null.

  • David Zweben 268 posts 754 karma points
    May 12, 2022 @ 21:56
    David Zweben
    0

    My apologies for the scare, the issue wasn't related to your code. Instead, it seems to be related to one of the additional properties I added to the Image media type. Using a media type with only standard properties got rid of the issue.

  • David Zweben 268 posts 754 karma points
    May 12, 2022 @ 21:29
    David Zweben
    0

    Actually, it may be that the issue with RTE images being cleared out is unrelated... it seems strange, but even after disabling app.UseCustomMediaAuthentication() in Startup.cs and restarting the site, I'm still having the issue. So it seems like there must be something else going on.

  • Jamie Attwood 208 posts 504 karma points c-trib
    May 13, 2022 @ 12:38
    Jamie Attwood
    0

    Thanks David, glad to hear that!

  • Jonathan Roberts 409 posts 1063 karma points
    Feb 02, 2023 @ 11:07
    Jonathan Roberts
    0

    Hi - Thanks for the code - but when I browse to a media file for example: https://localhost:44333/media/63636/mytestfile.doc - as it exists the Middleware code doesn't get hit. The file automatically downloads.

    But if I use a deliberate incorrect URL - still using media in the URL the code is hit.

    Any ideas?

  • Jamie Attwood 208 posts 504 karma points c-trib
    Feb 02, 2023 @ 16:16
    Jamie Attwood
    0

    Just clear your cache. If the file exists locally in cache the browser will not initiate a request for that item and instead serve the media directly from cache. This is browser behavior, not middleware related.

    In a real-world situation the end-user would not have protected media in the local cache unless they had permissions initially to access it. The first request would hit the middleware. You can always force the request by tagging on a datetimestamp in a querystring in the URL for that media item. Hope this is a solution to your issue!

    Cheers,

    Jamie

  • Jonathan Roberts 409 posts 1063 karma points
    Feb 02, 2023 @ 16:18
    Jonathan Roberts
    0

    Yes - that did it - many thanks

  • Jonathan Roberts 409 posts 1063 karma points
    Feb 07, 2023 @ 10:17
    Jonathan Roberts
    0

    Hi - if the code takes the user to the document the document doesnt download but takes them to a blank page.

    If the doc is not allowed to view or in recycle bin - then I get a "failed" response in the Network instead of a 404

  • Jamie Attwood 208 posts 504 karma points c-trib
    Feb 07, 2023 @ 13:48
    Jamie Attwood
    0

    Hi Jonathan, all this class does is sit in the pipeline and allows any request to continue on if the request path contains "/media/*" and if the member is authenticated. It does not process the media or provide any response other than a 401 (not authorized) if the member is not logged in. All routing/resolution occurs after this gateway allows the request to pass on.

    Note also, that this only checks members are authenticated (not back-office users) although you can add in that check as well.

    You could do a check in your view to perform some other behavior based on a 401 response from the media item if you wanted to I guess. I really don't have much info on what you are trying to achieve.

    Thanks,

    Jamie

  • Jonathan Roberts 409 posts 1063 karma points
    Feb 07, 2023 @ 14:00
    Jonathan Roberts
    0

    But if they are authorized, in your code, does the Media file still download OK? Im getting a failure even if the user is OK to view the document.

  • Jamie Attwood 208 posts 504 karma points c-trib
    Feb 07, 2023 @ 14:05
    Jamie Attwood
    1

    If the member is logged in, then request is allowed to continue forward and it's handled by the umbraco request pipeline. This code does not do any processing it's just a gate that either allows the request to complete or not based on login. Likely you are experiencing something higher up the foodchain. As you say, the media is in the recycle bin, deleted, etc. Again, I would need to see the code that is failing. Cheers!

    Jamie

  • Yashwin2492 8 posts 28 karma points
    Aug 01, 2024 @ 14:36
    Yashwin2492
    0

    I have similar kind of implementation in my project, Trying to protect some media files from unauthorised users.

    But those protected files are not loading up in the Umbraco backoffice, Is there a way to find the request from backoffice?

    Tried using "backOfficeSecurityAccessor.BackOfficeSecurity.IsAuthenticated()" , But its always false.

  • David Zweben 268 posts 754 karma points
    Aug 01, 2024 @ 14:43
    David Zweben
    0

    I believe we just have logic to check if the request URL's path starts with "/umbraco" and exclude those from further actions.

  • Yashwin2492 8 posts 28 karma points
    Aug 01, 2024 @ 14:51
    Yashwin2492
    0

    The URL path actually starts with something like this "/media/fbndt65v/{filename}.{fileExtn}"

  • David Zweben 268 posts 754 karma points
    Aug 01, 2024 @ 14:54
    David Zweben
    0

    Yes, I believe you would have to reference the referring URL for the request, which should be "/umbraco#/something", rather than the request URL itself.

  • Yashwin2492 8 posts 28 karma points
    Aug 01, 2024 @ 15:19
    Yashwin2492
    0

    The referring URL is always the backoffice url (http://localhost:1234/umbraco).

    In that case whenever I try to open that file from the backoffice, the validation will be ignored.

  • Jamie Attwood 208 posts 504 karma points c-trib
    Aug 01, 2024 @ 14:57
    Jamie Attwood
    0

    Right, since it's middleware, it's examining the current request for media.

    backOfficeSecurityAccessor.BackOfficeSecurity.IsAuthenticated() occurs later in the request lifecycle so it wont work here.

    Here is how I was able to achieve this:

     var backofficeLoggedIn = false;
     CookieAuthenticationOptions cookieOptions = _cookieAuthenticationsOptionsSnapshot.Get(Umbraco.Cms.Core.Constants.Security.BackOfficeAuthenticationType);
     string backOfficeCookie = context.Request.Cookies[cookieOptions.Cookie.Name!];
     if (!string.IsNullOrEmpty(backOfficeCookie))
     {
         AuthenticationTicket unprotected = cookieOptions.TicketDataFormat.Unprotect(backOfficeCookie!);
         ClaimsIdentity backOfficeIdentity = unprotected.Principal.GetUmbracoIdentity();
         backofficeLoggedIn = backOfficeIdentity.IsAuthenticated;
     }
    

    Then:

    if (_memberManager.IsLoggedIn() || backofficeLoggedIn) //Authenticate user via memberManager through method DI OR if user is an admin so they don't get media blocked from within the backoffice OR you can check with this method if (context.User.Identities.Any(IdentityExtensions => IdentityExtensions.IsAuthenticated))
    {
        await _next(context);
        return;
    }
    
  • Yashwin2492 8 posts 28 karma points
    Aug 01, 2024 @ 15:21
    Yashwin2492
    0

    Thanks for the code snippet, But I couldn't use the "_cookieAuthenticationsOptionsSnapshot" property.

    CookieAuthenticationOptions doesn't contain a definition for 'Get'.

  • Jamie Attwood 208 posts 504 karma points c-trib
    Aug 01, 2024 @ 15:25
    Jamie Attwood
    0

    Hmm that works for Umbraco 10/11. What are you on 13? If so I will take a look later as we are currently porting to 13.

  • Yashwin2492 8 posts 28 karma points
    Aug 01, 2024 @ 15:27
    Yashwin2492
    0

    Yeah, We recently updated to v13.2.0

  • Jamie Attwood 208 posts 504 karma points c-trib
    Aug 01, 2024 @ 15:27
    Jamie Attwood
    1

    Also, you need to DI the IOptionsSnapshot into the method like this:

    public async Task InvokeAsync(
        HttpContext context,
        IMemberManager _memberManager,
        IOptionsSnapshot<CookieAuthenticationOptions> _cookieAuthenticationsOptionsSnapshot)
    {...}
    
  • Yashwin2492 8 posts 28 karma points
    Aug 01, 2024 @ 15:53
    Yashwin2492
    0

    It works fine in v13, Thanks you.

Please Sign in or register to post replies

Write your reply to:

Draft