I have a hosted blazor web assembly app where I want to secure file downloads so that only a user with the correct role / permissions can download a file from the folder. I do not want an anonymous user to be able to download any files from this folder.
The files are located in a folder named Uploads
in the server / api project, so they are not in the wwwroot
folder.
The server pipeline looks like:
...
app.UseAuthorization();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "Uploads")),
RequestPath = "/SellerFiles"
});
//app.Map("", appBuilder =>
//{
// appBuilder.UseFilter();
// appBuilder.UseFileServer(new FileServerOptions
// {
// FileProvider = new PhysicalFileProvider($@"{Path.Combine(builder.Environment.ContentRootPath, "Uploads")}"),
// RequestPath = new PathString("/SellerFiles"), //empty, because root path is in Map now
// EnableDirectoryBrowsing = false
// });
//});
app.MapControllers();
...
UseFilters
looks like
public class FilterMiddleware
{
private readonly RequestDelegate _next;
public FilterMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
List<Claim> claims = httpContext.User.Claims.ToList();
await _next(httpContext);
//if (true)
//{
// //proceed serving files
// await _next(httpContext);
//}
//else
//{
// //return you custom response
// await httpContext.Response.WriteAsync("Forbidden");
//}
}
}
public static class FilterMiddlewareExtensions
{
public static IApplicationBuilder UseFilter(this IApplicationBuilder applicationBuilder)
{
return applicationBuilder.UseMiddleware<FilterMiddleware>();
}
}
When a user requests a file from https://mywebsite.com/SellerFiles/...
, I want to serve the file if they are authenticated, but not if they are anonymous.
I tried implementing the commented code app.Map UseFileServer
, but I wasn't able to configure it correctly. I played around with it alot and I was not able to get it to hit a breakpoint in public async Task InvokeAsync
when I made a request to /SellerFiles
.
Right now, an authenticated user can download a file from the Uploads
folder by making a call to /SellerFiles
, but so can an anonymous user.
How can I properly protect the files?
Edit I think I found the problem, but I am not quite there yet. I followed the guidance here: Microsoft Link to Static File Authorization
I set the authorization fallback policy, and it works well. I had to make some controller methods AllowAnonymous. So now the Uploads folder should require authorization, but I can still download files when not logged in.
builder.Services.AddAuthorizationCore(options =>
{
var type = typeof(Permissions);
foreach (var permission in type.GetFields())
{
options.AddPolicy(
permission.GetValue(null)?.ToString() ?? "",
policyBuilder => policyBuilder.RequireAssertion(
context => context.User.HasClaim(claim => claim.Type == "Permissions" && claim.Value == permission.GetValue(null)?.ToString())));
}
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseResponseCompression();
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseSerilogRequestLogging();
app.UseStaticFiles();
app.UseAuthorization();
app.UseBlazorFrameworkFiles();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.MapHangfireDashboard();
app.MapHub<SignalR>("/chathub");
using (var scope = app.Services.CreateScope())
{
var jobService = scope.ServiceProvider.GetRequiredService<IScheduleJobs>();
jobService.Run();
//do your stuff....
}
var options = new DashboardOptions
{
Authorization = new IDashboardAuthorizationFilter[]
{
new HangfireDashboardJwtAuthorizationFilter(tokenValidationParameters, "Administrator")
}
};
app.UseHangfireDashboard("/hangfire", options);
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "Uploads")),
RequestPath = "/SellerFiles"
});
app.Run();
What am I missing?
Well, I solved my issue and wanted to show the solution. I ended up calling an API endpoint and streaming the file back to the client. Then I use a simple Javascript function to create an A tag
and invoke the download. The user chooses where to save the file. Also, I removed all of the modifications I had made to Program.cs.
This is my razor.cs code which is invoked by the user clicking the download button.
protected async Task DownloadFile(CartViewModel cartViewModel)
{
FileDownload fileDownload = new FileDownload()
{
OrderId = cartViewModel.OrderId,
SellerId = cartViewModel.Product?.SellerId,
ZipFileName = cartViewModel.Product?.ZipFileName
};
if(OrderService != null)
{
bool response = await OrderService.DownloadFileStream(fileDownload, $"{ApiEndpoints.File}/download/{cartViewModel.Product?.SellerId}/{cartViewModel.OrderId}", JSruntime);
if (response)
{
SnackbarService?.Add("File Downloaded Successfully", Severity.Success);
}
else
{
SnackbarService?.Add("Problem Downloading File", Severity.Error);
}
}
}
This is my OrderService.DownloadFileStream method.
public async Task<bool> DownloadFileStream(FileDownload model, string endPoint, IJSRuntime JSruntime)
{
try
{
var user = System.Text.Json.JsonSerializer.Serialize(model);
var requestContent = new StringContent(user, Encoding.UTF8, "application/json");
if (!await GetBearerToken())
{
return false;
}
Stream stream = await client.GetStreamAsync(endPoint);
byte[] bytes;
using (var memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
bytes = memoryStream.ToArray();
}
string base64 = Convert.ToBase64String(bytes);
await JSruntime.InvokeAsync<object>("saveAsFile", model.ZipFileName, base64);
}
catch (ApiException exception)
{
return false;
}
return true;
}
This is my controller endpoint.
[HttpGet]
[Authorize(Policy = Permissions.PurchaseProducts)]
[Route("download/{SellerId}/{OrderId:int}")]
public async Task<MemoryStream> Get([FromRoute] string SellerId, [FromRoute] int OrderId)
{
string filePath = string.Empty;
DirectoryInfo d = new DirectoryInfo(@$"Uploads\{SellerId}\{OrderId}\");
FileInfo[] files = d.GetFiles();
// There should only be 1 file in this folder
foreach (FileInfo fileInfo in files)
{
if (fileInfo.Extension.ToLower().Trim() == ".zip")
{
filePath = @$"Uploads\{SellerId}\{OrderId}\{fileInfo.Name}";
}
}
if(filePath == string.Empty)
{
return new MemoryStream();
}
//converting Pdf file into bytes array
var dataBytes = await System.IO.File.ReadAllBytesAsync(filePath);
//adding bytes to memory stream
var dataStream = new MemoryStream(dataBytes);
return dataStream;
}
And this is the Javascript function.
function saveAsFile(filename, bytesBase64) {
var link = document.createElement('a');
link.download = filename;
link.href = "data:application/octet-stream;base64," + bytesBase64;
document.body.appendChild(link); // Needed for Firefox
link.click();
document.body.removeChild(link);
}
Since the files are in an Uploads folder outside of wwwroot, users cannot get to https://mywebsite.com/Uploads
or any subfolders, so my files are protected. I plan to add checks in the controller endpoint to ensure that the user really is authorized to download the file, i.e., they paid for it.