How to return link to file in ASP Net Core? - asp.net-core

I do not know how to serve links to files in ASP Net Core.I have tried using the PhyisicalFileProvider class to no avail.
What i want
Given a folder root on the hard disk , on receiving a querystring
like : /localFolder/test.txt i want the server to send a Link so
the user can click and get the file test.txt
Important
I do not want to send the file but a link to it , so he can click it and download it.
What i have tried:
1.Using the extension method IApplicationBuilder.Map in order to direct the requests directly to the file.
2.Using the extension method IApplicationBuilder.Map + Adding a middleware ,though i do not know how to i serve the link ? (add it
in the response body?)
Startup
public void ConfigureServices(IServiceCollection collection)
{
//i have also added the provider to the service collection to further inject it in the middleware
var phyisicalFileProvider = new PhysicalFileProvider(config.Storage.DocxFileRoot);
services.AddSingleton<IFileProvider>(phyisicalFileProvider);
}
public void Configure()
{
//scenario without middleware
app.Map("/localfiles", x =>
x.UseStaticFiles(new StaticFileOptions {
FileProvider = new PhysicalFileProvider([some file root]),RequestPath ="/localfiles"}
));
//scenario with middleware
app.Map("/localfiles",x=>
x.UseMiddleware<FileWare>()
);
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=File}/{action=Index}/{id?}");
});
}
Middleware
public class FileWare
{
private IFileProvider provider;
private RequestDelegate next;
public FileWare(RequestDelegate next,IFileProvider provider)
{
this.provider=provider;
this.next=next;
}
public async Task Invoke(HttpContext context)
{
var query = context.Request.Query;
var path=query.First(x => x.Key == "path").Value;
var fileInfo=this.provider.GetFileInfo(path);
await fileInfo.CreateReadStream().CopyToAsync(context.Response.Body);
}
}

If you just want to have a link in razor view,click the link to download a file in the local disk(for example C:\MyFolder), you could follow below steps:
1.Razor View
<a asp-controller="File" asp-action="download" asp-route-path="C:\MyFolder\test.txt">Download</a>
2.FileController
public IActionResult Download([FromQuery]string path)
{
string fileName = "test.txt";
byte[] fileBytes = System.IO.File.ReadAllBytes(path);
return File(fileBytes, "application/force-download", fileName);
}

Code below helps to serve files to users in ASP NET Core:
in program.cs write this:
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "RealFilePath")),
RequestPath = "/NickNameForPath"
});
then in yourpage.cshtml, code this link:
Get File
And do not forget place folders under wwwroot

Related

Custom Login Path for ASP.NET CORE 6 and Azure AD

I currently have a page in "/login/index" with my logo and a form/button on it which on POST will initiate the challenge for MS Azure AD login to authenticate the user. The user is then redirected back to the home page after login. However, currently with the default setup for Azure AD authentication a user never sees this "/login/index" page because they are forced to MS Azures ADs login page for all request paths if not authenticated. Is there a way to force users to this initial login page I setup so that they can click the button to go authenticate?
My program.cs is as follows:
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using Project.Models;
using DocumentFormat.OpenXml.Office2016.Drawing.ChartDrawing;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
});
//authentication pipline
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
//We are using this so we can find the modified date later on. If we move to Box or Onedrive we may not need this.
var RootPath = builder.Environment.ContentRootPath;
var WebPath = builder.Environment.WebRootPath;
var fileDirectory = Path.Combine(Directory.GetParent(RootPath).Parent.ToString(), "files");
IFileProvider physicalProvider = new PhysicalFileProvider(fileDirectory);
builder.Services.AddSingleton<IFileProvider>(physicalProvider);
//Not needed. We are not using this level of abstraction but may move towards it one day so possibly keep.
var connectionString = builder.Configuration.GetConnectionString("DBContext");
builder.Services.AddDbContext<DbContext>(options => options.UseSqlServer(connectionString));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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();
}
else
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
//We are making it so armsfiles are not accessible outside of arms so if we move to box or onedrive then the parameter may need to be removed.
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = physicalProvider,
RequestPath = "/files"
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
app.Run();
Before using Azure AD for authentication I would use this cookie policy in my startup class (I have since converted to minimal hosting model in program.cs) to force users to the login page (which although different now but still similar concept to what I am trying to achieve):
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(cookieOptions =>
{
cookieOptions.Cookie.Name = "UserLoginCookie";
cookieOptions.LoginPath = "/Login/";
cookieOptions.ExpireTimeSpan = TimeSpan.FromMinutes(30);
cookieOptions.SlidingExpiration = true;
});
I follow the official doc about Quickstart: Add sign-in with Microsoft to a web app. And I downloaded the repo and test it.
You can check the test result first, please confirm if this is what you want?
Test Result
What I changed in the sample project
Change the appsettings.json file in project.
Copy the code from Home/Index method, and create a new page LoginSuccess then paste the code to Home/LoginSuccess. And modify the attribute, you can copy my sample code directly.
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Web;
using Microsoft.Graph;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using active_directory_aspnetcore_webapp_openidconnect_v2.Models;
namespace active_directory_aspnetcore_webapp_openidconnect_v2.Controllers
{
[Authorize]
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly GraphServiceClient _graphServiceClient;
public HomeController(ILogger<HomeController> logger,
GraphServiceClient graphServiceClient)
{
_logger = logger;
_graphServiceClient = graphServiceClient;
}
[AllowAnonymous]
public async Task<IActionResult> Index()
{
//var user = await _graphServiceClient.Me.Request().GetAsync();
ViewData["ApiResult"] = "Demo";//user.DisplayName;
return View();
}
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
public async Task<IActionResult> LoginSuccess()
{
var user = await _graphServiceClient.Me.Request().GetAsync();
ViewData["ApiResult"] = user.DisplayName;
return View();
}
[AuthorizeForScopes(ScopeKeySection = "DownstreamApi:Scopes")]
public IActionResult Privacy()
{
return View();
}
[AllowAnonymous]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
Create LoginSuccess page(copy from index page).

Blazor server and API in same project, 404 not found when app.UserAuth is activate

I have a Blazor server project with some API controllers in same project.
In my Program.cs I have this code :
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
..
app.UseAuthentication();
app.UseAuthorization();
When I call my API from my blazor component I got 404 not found response.
If I comment out app.UseAuthentication() and app.UseAuthorization my component can call my API and it works.
I'm a newbie on auth and API and don't know where to start.
My API has no [Auth] tags in it. I can reach the API with Swagger without problems.
My code in component (it works without "UseAuth" but not when it's activate):
string filnamn = WebUtility.UrlEncode(fil.Namn);
string reqUri = $"delete/{filnamn}";
Http.BaseAddress = new Uri("https://localhost:7285/");
Http.DefaultRequestHeaders.Accept.Clear();
HttpResponseMessage response = await Http.DeleteAsync(reqUri);
My API controller :
[ApiController]
public class UploadController : ControllerBase
{
private readonly string grundPath = #"G:\Testfolder";
private readonly string ulPath = "Upload";
[HttpDelete("delete/{filename}")]
public IActionResult Delete(string filename)
{
try
{
var filePath = Path.Combine(grundPath, ulPath, filename);
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
return StatusCode(200);
}
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
return StatusCode(500);
}
Do you see some obvious wrong/missing part I do or could give me some direction on what I should google for?

How to use fallback routing in asp.net core?

I am using asp.net web-api with controllers.
I want to do a user section where one can request the site's address with the username after it like example.com/username. The other, registered routes like about, support, etc. should have a higher priority, so if you enter example.com/about, the about page should go first and if no such about page exists, it checks whether a user with such name exists. I only have found a way for SPA fallback routing, however I do not use a SPA. Got it working manually in a middleware, however it is very complicated to change it.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string[] internalRoutes = new string[] { "", "about", "support", "support/new-request", "login", "register" };
string[] userNames = new string[] { "username1", "username2", "username3" };
app.Use(async (context, next) =>
{
string path = context.Request.Path.ToString();
path = path.Remove(0, 1);
path = path.EndsWith("/") ? path[0..^1] : path;
foreach (string route in internalRoutes)
{
if (route == path)
{
await context.Response.WriteAsync($"Requested internal page '{path}'.");
return;
}
}
foreach (string userName in userNames)
{
if (userName == path)
{
await context.Response.WriteAsync($"Requested user profile '{path}'.");
return;
}
}
await context.Response.WriteAsync($"Requested unknown page '{path}'.");
return;
await next(context);
});
app.Run();
It's really straightforward with controllers and attribute routing.
First, add controller support with app.MapControllers(); (before app.Run()).
Then, declare your controller(s) with the appropriate routing. For simplicity, I added a single one that just returns simple strings.
public class MyController : ControllerBase
{
[HttpGet("/about")]
public IActionResult About()
{
return Ok("About");
}
[HttpGet("/support")]
public IActionResult Support()
{
return Ok("Support");
}
[HttpGet("/support/new-request")]
public IActionResult SupportNewRequest()
{
return Ok("New request support");
}
[HttpGet("/{username}")]
public IActionResult About([FromRoute] string username)
{
return Ok($"Hello, {username}");
}
}
The routing table will first check if there's an exact match (e.g. for /about or /support), and if not, if will try to find a route with matching parameters (e.g. /Métoule will match the /{username} route).

How to force re authentication between ASP Net Core 2.0 MVC web app and Azure AD

I have an ASP.Net Core MVC web application which uses Azure AD for authentication. I have just received a new requirement to force user to reauthenticate before entering some sensitive information (the button to enter this new information calls a controller action that initialises a new view model and returns a partial view into a bootstrap modal).
I have followed this article which provides a great guide for achieving this very requirement. I had to make some tweaks to get it to work with ASP.Net Core 2.0 which I think is right however my problems are as follows...
Adding the resource filter decoration "[RequireReauthentication(0)]" to my controller action works however passing the value 0 means the code never reaches the await.next() command inside the filter. If i change the parameter value to say 30 it works but seems very arbitrary. What should this value be?
The reauthentication works when calling a controller action that returns a full view. However when I call the action from an ajax request which returns a partial into a bootstrap modal it fails before loading the modal with
Response to preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'https://localhost:44308' is therefore not allowed
access
This looks like a CORS issue but I don't know why it would work when going through the standard mvc process and not when being called from jquery. Adding
services.AddCors();
app.UseCors(builder =>
builder.WithOrigins("https://login.microsoftonline.com"));
to my startup file doesn't make any difference. What could be the issue here?
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Ommitted for clarity...
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();
services.AddCors();
// Ommitted for clarity...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Ommitted for clarity...
app.UseCors(builder => builder.WithOrigins("https://login.microsoftonline.com"));
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
AzureAdAuthenticationBuilderExtensions.cs
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
=> builder.AddAzureAd(_ => { });
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
builder.AddOpenIdConnect(options =>
{
options.ClaimActions.Remove("auth_time");
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = RedirectToIdentityProvider
};
});
return builder;
}
private static Task RedirectToIdentityProvider(RedirectContext context)
{
// Force reauthentication for sensitive data if required
if (context.ShouldReauthenticate())
{
context.ProtocolMessage.MaxAge = "0"; // <time since last authentication or 0>;
}
else
{
context.Properties.RedirectUri = new PathString("/Account/SignedIn");
}
return Task.FromResult(0);
}
internal static bool ShouldReauthenticate(this RedirectContext context)
{
context.Properties.Items.TryGetValue("reauthenticate", out string reauthenticate);
bool shouldReauthenticate = false;
if (reauthenticate != null && !bool.TryParse(reauthenticate, out shouldReauthenticate))
{
throw new InvalidOperationException($"'{reauthenticate}' is an invalid boolean value");
}
return shouldReauthenticate;
}
// Ommitted for clarity...
}
RequireReauthenticationAttribute.cs
public class RequireReauthenticationAttribute : Attribute, IAsyncResourceFilter
{
private int _timeElapsedSinceLast;
public RequireReauthenticationAttribute(int timeElapsedSinceLast)
{
_timeElapsedSinceLast = timeElapsedSinceLast;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
var foundAuthTime = int.TryParse(context.HttpContext.User.FindFirst("auth_time")?.Value, out int authTime);
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (foundAuthTime && ts - authTime < _timeElapsedSinceLast)
{
await next();
}
else
{
var state = new Dictionary<string, string> { { "reauthenticate", "true" } };
await AuthenticationHttpContextExtensions.ChallengeAsync(context.HttpContext, OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state));
}
}
}
CreateNote.cs
[HttpGet]
[RequireReauthentication(0)]
public IActionResult CreateNote(int id)
{
TempData["IsCreate"] = true;
ViewData["PostAction"] = "CreateNote";
ViewData["PostRouteId"] = id;
var model = new NoteViewModel
{
ClientId = id
};
return PartialView("_Note", model);
}
Razor View (snippet)
<a asp-controller="Client" asp-action="CreateNote" asp-route-id="#ViewData["ClientId"]" id="client-note-get" data-ajax="true" data-ajax-method="get" data-ajax-update="#client-note-modal-content" data-ajax-mode="replace" data-ajax-success="ShowModal('#client-note-modal', null, null);" data-ajax-failure="AjaxFailure(xhr, status, error, false);"></a>
All help appreciated. Thanks
The CORS problem is not in your app.
Your AJAX call is trying to follow the authentication redirect to Azure AD,
which will not work.
What you can do instead is in your RedirectToIdentityProvider function, check if the request is an AJAX request.
If it is, make it return a 401 status code, no redirect.
Then your client-side JS needs to detect the status code, and issue a redirect that triggers the authentication.

ASP.NET Core 1.0 custom compression middleware and static files issue

My startup's middleware configuration looks something like this:
public void Configure(IApplicationBuilder app)
{
app.UseCompression();
app.UseIISPlatformHandler();
app.UseApplicationInsightsRequestTelemetry();
app.UseCors("CorsPolicy");
app.UseStaticFiles();
app.UseMvc();
app.UseApplicationInsightsExceptionTelemetry();
}
Since adding the app.UseCompression() middleware, static html files in wwwroot aren't loading correctly anymore. They don't resolve and tend to load indefinitely.
The compression middleware looks as follows and was sourced from here:
public class CompressionMiddleware
{
private readonly RequestDelegate nextDelegate;
public CompressionMiddleware(RequestDelegate next)
{
nextDelegate = next;
}
public async Task Invoke(HttpContext httpContext)
{
var acceptEncoding = httpContext.Request.Headers["Accept-Encoding"];
//Checking that we have been given the correct header before moving forward
if (!(String.IsNullOrEmpty(acceptEncoding)))
{
//Checking that it is gzip
if (acceptEncoding.ToString().IndexOf("gzip", StringComparison.CurrentCultureIgnoreCase) >= 0)
{
using (var memoryStream = new MemoryStream())
{
var stream = httpContext.Response.Body;
httpContext.Response.Body = memoryStream;
await nextDelegate(httpContext);
using (var compressedStream = new GZipStream(stream, CompressionLevel.Optimal))
{
httpContext.Response.Headers.Add("Content-Encoding", new string[] { "gzip" });
memoryStream.Seek(0, SeekOrigin.Begin);
await memoryStream.CopyToAsync(compressedStream);
}
}
}
else
{
await nextDelegate(httpContext);
}
}
//If we have are given to Accept Encoding header or it is blank
else
{
await nextDelegate(httpContext);
}
}
}
Does anyone know why this could be happening?
Note: I am using DNX 1.0.0-rc1-update1 and the 1.0.0-rc1-final libraries.
I just enabled compression in web.config in /wwwroot folder (like we used to do in IIs applicationHost.config), and it works. There's no need to add a compression middleware.
http://www.brandonmartinez.com/2015/08/20/enable-gzip-compression-for-azure-web-apps/
Compression defined in web.config only works with IIS NOT the self hosted web server.
I tested the CompressionMiddleware example and the problem You are seeing is caused by the Content-Length header.
If the Content-Length is already set, the browser gets confused as the actual size of the response doesn't match the value defined in the header as the content is zipped.
Removing the content-length header solved the problem:
httpContext.Response.Headers.Remove("Content-Length");
httpContext.Response.Headers.Add("Content-Encoding", new string[] { "gzip" });
...even better you could try to specify the actual content-length when the content is zipped.