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:
"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?
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.
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.
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.
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();
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;
}
}
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.
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.
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.
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.
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!
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.
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!
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.
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;
}
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:
It is being called in startup.cs here in the "WithMiddleWare" method:
1,2) try this:
And remember to clear the cache!
"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?
I am trying to do this exact same thing right now, so I’m watching this thread with great interest.
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.
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.
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
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)
The extension class (CustomMediaAuthenticationExtension.cs)
And the call in Startup.cs:
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:
Cheers!
Jamie
This is great! Worked perfectly.
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.
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)
Unfortunately,
backOfficeSecurityAccessor.BackOfficeSecurity.IsAuthenticated()
is returning False even from a backoffice context inside this method.context.User.GetUmbracoIdentity();
is also null.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.
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.
Thanks David, glad to hear that!
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?
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
Yes - that did it - many thanks
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
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
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.
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
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.
I believe we just have logic to check if the request URL's path starts with "/umbraco" and exclude those from further actions.
The URL path actually starts with something like this "/media/fbndt65v/{filename}.{fileExtn}"
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.
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.
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:
Then:
Thanks for the code snippet, But I couldn't use the "_cookieAuthenticationsOptionsSnapshot" property.
CookieAuthenticationOptions doesn't contain a definition for 'Get'.
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.
Yeah, We recently updated to v13.2.0
Also, you need to DI the IOptionsSnapshot into the method like this:
It works fine in v13, Thanks you.
is working on a reply...