Role based Access to Static Content in ASP.NET CORE - asp.net-core

Is there any way I can give access to static content based on their role. In my application, the authentication is done through Azure Active AD and the contents should be accessed based on their role e.g. employee should access all employee pages and students should access student pages. This is how my solution explorer looks like.
Solution Explorer
I know this is duplicate of Secure requests to .html files in ASP.NET Core but I couldn't find any approach to implement the solution. I have made new folder intranet outside the wwwRoot to serve my static content but still need to know how can I authorize the user and and serve role based static files.

As the document said, you could store the static files outside of wwwroot and any directory accessible to the Static File Middleware (for example: MyStaticFiles folder, like this), then, you could serve them via an action method to which authorization is applied and return a FileResult object:
[Authorize(Roles = "User")]
public IActionResult BannerImage()
{
var filePath = Path.Combine(
_env.ContentRootPath, "MyStaticFiles", "images", "Image1.jpg");
return PhysicalFile(filePath, "image/jpeg");
}
Then, you could view image by click the following link:
<a asp-action="BannerImage" asp-controller="Home">View Image</a>
[Note] After using the above method, if the authorize not working, try to clear the cache, perhaps the issue is related to the browser cache. Besides, if you meet the "HTTP Error 404.15 - Not Found" error, try to add [AllowAnonymous] attribute for other controller action method.

Related

Asp .net core - Authorize access to all files in a folder via roles

I'm upgrading some initial razor code into asp .net razor pages with .net core 5.0. I've been through many examples on the microsoft site, but it seems that I have to set attributes in all of my .cshtml.cs files. that feels just sloppy and error prone because something will be forgotten somewhere.
In .net 4.x razor, I have an _PageStart.cshtml file, I check the user's role, and I redirect them to the login page if they are not in a particular role. I'd like to do the same in asp .net core using a single file or configuration. I don't want to put an attribute on every pagemodel file, that just seems sloppy. I imagine that I would do something like:
options.Conventions.AuthorizeFolder("/Club", "ClubAdmin");
where ClubAdmin is a role in the application and Club is a folder that contains a bunch of razor pages and sub folders. Is this possible?
TIA
To do this, you can define a policy in your Startup.cs file that checks for a role and then configure razor pages to Authorize that folder for that specific policy:
//define the admin policy
services.AddAuthorization(options =>
{
options.AddPolicy("AdminPolicy", policy => policy.RequireRole("Administrator"));
});
services.AddRazorPages(options =>
{
options.Conventions.AuthorizeFolder("/Admin", "AdminPolicy");
});
The RequireRole extension method injects a RolesAuthorizationRequirement handler that will validate for the given role during authorization

ASP.NET Identity redirect to full URL after login

I have a Blazor website (IdentityApp) that includes scaffolded ASP.NET identity integration. That website is processing logins and setting a shared cookie that my other localhost Blazor application (LogicApp) can see.
When I access LogicApp (https://localhost:9876), I check the context for a recognised user (using the shared cookie) and then manually redirect anonymous users to the scaffolded IdentityApp login page: https://localhost:1234/identity/account/login.
I also pass through the fully encoded URL of LogicApp home page in the returnUrl, for example; https://localhost:1234/identity/account/login?returnUrl=https%3A%2F%2Flocalhost%3A9876%2F
The login process is working OK (the shared cookie is set correctly), but then I get an exception;
InvalidOperationException: The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local.
Within ASP.NET Identity (.NET Core 3.1);
How can I configure IdentityApp to allow full urls within returnUrl?
or,
How can I override the scaffolded Login logic to take control of the redirect and use my returnUrl value?
So I found an answer to my own problem (but still interested to hear of alternative solutions)..
Within the scaffolded pages locate Login.cshtml.cs and replace the reference to LocalRedirect(...) in in the OnPostAsync(string returnUrl = null) method, for example;
...
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
//return LocalRedirect(returnUrl); // remove this.
return Redirect(returnUrl); //TODO: define a list of acceptable return Urls (localhost / yourdomain.com etc)
}
...
You may also need to do this in Login2FA.cshtml.cs as well as other places that deal with this ReturnUrl value.
NOTE: LocalRedirect is used to prevent malicious redirects occurring so it would be wise to also add some logic to check the returnUrls that have been passed in.

Download File in Blazor Server Side with Authentication

I have a Blazor Server Side Web Application that uses the default authorization and authentication.
app.UseAuthentication()
app.UseAuthorization()
I can protect my pages with
#attribute [Authorize]
I have a login page with anonymous access to authenticate. This works fine.
Now I need a way to let the user download files from this authorized pages. Surprisingly I haven't found any straightforward way to do this.
One workaround is to build an API Controller with the filename as a path argument and give the user a link to it.
[Route("api/[controller]")]
public class FileController{
[HttpGet("download/{filename}")]
public async Task<IActionResult> Download([FromRoute] string filename){
//Do some checks and get file from Filesystem
return file;
}
}
And in the .razor file
private string CalculateDownloadLink(string filename){
return $"{NavigationManager.BaseUri}/api/file/download/{filename}"
}
This is a dumbed down version. In reality the filenames are generic. This works too.
Now I want to add Authentication to the API Controller because I don't want anyone guessing filenames. But I don't know how.
Of Course the [Authorize] Attribute doesn't work because the code is outside the circuit scope.
I can't figure out how to use any build-in Authorization to make this work.
Is there a better way to download files from a Blazor app?
A bit late answering your question, but [Authorize] on the controller should work. Have you tried it? Controller methods get the same cookies that Blazor pages get.
But there is a better way to download a file from Blazor without a need for navigation. See this blog post.
Your controller is not a controller. It does not implement Controller...
[Authorize]
public class FileController : Controller
{
...
}
FYI: If you add the download attribute to the anchor the file will only download when clicked.
<a download href="#CalculateDownloadLink("file.txt")">Download file.txt</a>
I made a junk repo that works if you need me to post it.

Access Denied query string too long

I'm developing an Asp.Net Core application. I'm using built-in Identity for login, roles, authorization and authentication. I'm developing/testing/debugging with IIS Express on a Windows machine. When I'm logged in as a non-admin user and try to navigate to a url that only admins have access to (Authorize attribute on the whole controller), the application redirects to an access denied URL, but then I get an error message saying that the url query string is too long. Upon inspecting the url, it appears to have repeated sections. I'll try to paste it below. Should I report this as a bug, or can I change a setting to prevent it?
https://localhost:44383/Account/AccessDenied?ReturnUrl=%2FAccount%2FAccessDenied%3FReturnUrl%3D%252FAccount%252FAccessDenied%253FReturnUrl%253D%25252FAccount%25252FAccessDenied%25253FReturnUrl%25253D%2525252FAccount%2525252FAccessDenied%2525253FReturnUrl%2525253D%252525252FAccount%252525252FAccessDenied%252525253FReturnUrl%252525253D%25252525252FAccount%25252525252FAccessDenied%25252525253FReturnUrl%25252525253D%2525252525252FAccount%2525252525252FAccessDenied%2525252525253FReturnUrl%2525252525253D%252525252525252FAccount%252525252525252FAccessDenied%252525252525253FReturnUrl%252525252525253D%25252525252525252FAccount%25252525252525252FAccessDenied%25252525252525253FReturnUrl%25252525252525253D%2525252525252525252FAccount%2525252525252525252FAccessDenied%2525252525252525253FReturnUrl%2525252525252525253D%252525252525252525252FAccount%252525252525252525252FAccessDenied%252525252525252525253FReturnUrl%252525252525252525253D%25252525252525252525252FAccount%25252525252525252525252FAccessDenied%25252525252525252525253FReturnUrl%25252525252525252525253D%2525252525252525252525252FAccount%2525252525252525252525252FAccessDenied%2525252525252525252525253FReturnUrl%2525252525252525252525253D%252525252525252525252525252FAccount%252525252525252525252525252FAccessDenied%252525252525252525252525253FReturnUrl%252525252525252525252525253D%25252525252525252525252525252FAccount%25252525252525252525252525252FAccessDenied%25252525252525252525252525253FReturnUrl%25252525252525252525252525253D%2525252525252525252525252525252FAccount%2525252525252525252525252525252FAccessDenied%2525252525252525252525252525253FReturnUrl%2525252525252525252525252525253D%252525252525252525252525252525252FAccount%252525252525252525252525252525252FAccessDenied%252525252525252525252525252525253FReturnUrl%252525252525252525252525252525253D%25252525252525252525252525252525252FAccount%25252525252525252525252525252525252FAccessDenied%25252525252525252525252525252525253FReturnUrl%25252525252525252525252525252525253D%2525252525252525252525252525252525252FAdmin%2525252525252525252525252525252525252FEditUser%2525252525252525252525252525252525252Ff61bbba3-42b5-4831-8ff1-4d92e42d5d99
The url you received you have an endless redirection loop, where each loops adds the url again and pass to itself.
In the default ASP.NET Core Identity templates, the AccountController is attributed with [Authorize] attribute, meaning any logged in user can access it.
The fact that you get redirected when trying to access /Account/AccessDenied route/action means the logged in use doesn't have the permission to access it.
This can happen when you use a different authentication scheme [Authorize(Scheme = "SomethingElse")] or as in your case (from your last comment) when a special group is required [Authorize(Roles = "Something")].
Even if you have some valid reason to change it on controller level, you should be able to set
[HttpGet]
[Authorize]
public IActionResult AccessDenied(string returnUrl) { ... }
To make an exception to it or use
[HttpGet]
[AllowAnonymous]
public IActionResult AccessDenied(string returnUrl) { ... }
which will allow any user to access it.

FileServerMiddleware with credentials?

We have the requirement in our enterprise environment to serve static file content from a network share in our ASP.NET Core application. Basically, it gets served under some sub path /content. For this, we have the following code, which works fine:
app.UseFileServer(new FileServerOptions
{
FileProvider = new PhysicalFileProvider("//our/network/share"),
RequestPath = new PathString("/content"),
EnableDirectoryBrowsing = false
});
Now in production the system user under whose context the web application is hosted has no access to the file share. Thus, we have to use a certain technical domain user to access the files and for this we have to provide credentials (username/password) of this system user to the file server.
Unfortunately, we did not find an option to provide credentials to UseFileServer(). Is it anyway possible?
According to the documentation for UseFileServer it combines the functionality of among other things UseStaticFiles. According to the middleware documentation, the static file module provides no auth checks. They do give you some options on how to accomplish file serving with authorization (again from the middleware docs):
If you want to serve files based on authorization:
Store them outside of wwwroot and any directory accessible to the static file middleware.
Deliver them through a controller action, returning a FileResult where authorization is applied.
Not sure how you are going to pass the username/password to the server. If you plan to use something like basic authentication (and don't want to use the methods outlined above), you can probably modify the headers (when serving the static files) to accomplish the desired effect, but that is a workaround and probably not a good idea.
I would use middleware to protect contents. I will try to write simple example(I assumed you are using any authentication middleware to authenticate your users and my example is for static files).
-- Below code is untested and is just for an illustration--
First, you need to create a middleware something like this:
public class ProtectFileMiddleware
{
private readonly RequestDelegate _next;
public ProtectFileMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/protected"))
{
if (!context.User.IsInRole("Admin"))
{
await context.Authentication.ChallengeAsync();
return;
}
}
await _next(context);
}
}
and use this middleware like below:
public void Configure(IApplicationBuilder app)
{
app.Use(?)Authentication();// it depends on your design
app.UseMiddleware<ProtectFileMiddleware>();
app.UseStaticFiles();
// other
}
Result: if you try to access /protected url as an admin user, you will get expected response otherwise you will take a 401/403 response.
For more flexible way take a look at http://odetocode.com/blogs/scott/archive/2015/10/06/authorization-policies-and-middleware-in-asp-net-5.aspx
Yeah, those answers assume you're asking about client credentials. What you really need is a IFileProvider implementation that has credentials to access a specific resource. I don't know that .NET is very good at accessing files as different users, it usually relies on impersonation. See How to present credentials in order to open file?