Blazor Redirection on IIS swagger - asp.net-core

I have a .NET 5 blazor WASM (with core server) solution.
I added swagger (nswag) like this:
public class Startup {
public void ConfigureServices(IServiceCollection services) {
services.AddControllersWithViews();
services.AddRazorPages();
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
services.AddSwaggerDocument(); //SWAGGER
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if(env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else {
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.UseBlazorFrameworkFiles();
app.UseStaticFiles();
// Register the Swagger generator and the Swagger UI middlewares
app.UseOpenApi();
app.UseSwaggerUi3();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
When I debug the appliation with IIS-Express and enter the address https://localhost:12234/swagger the swagger UI is displayed correctly.
But after deployment to IIS every address loads the blazor UI with "Sorry there is nothing at this address" instead of the swagger UI.
When I use an old IE (not able to run wasm) I get at least a title from swagger - so swagger is there on the server, but some "magic redirection" forces index.html to be loaded - no matter what I do.
By the way - I can call controller methods and a curl .../swagger/v1/swagger.json also works as expected.
How can I tell the app to accept URLs from the address line without redirection to index.html?
I use PWA and https in my project.

I found the solution.
There is a service-worker.published.js as a "subfile" in wwwroot/service-worker.js.
And there is code like this:
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
const shouldServeIndexHtml = event.request.mode === 'navigate';
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}
After a little change everthing works fine now:
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
const shouldServeIndexHtml = event.request.mode === 'navigate' && !event.request.url.includes('/swagger') && !event.request.url.includes('/api/');
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}
Adding two more conditions to shouldServeIndexHtml solved the problem.
const shouldServeIndexHtml = event.request.mode === 'navigate' && !event.request.url.includes('/swagger') && !event.request.url.includes('/api/');

Related

Blazor server, call api controller delete/{filename} to delete file. 404 response

I have an api that works in most functions, but not on my HttpDelete where I got 404 response.
[Route("/[controller]")]
[ApiController]
public class UploadController : ControllerBase
..
[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);
}
My Blazor component :
..
#inject HttpClient Http
..
string url = $"delete/{filename}"
HttpResponseMessage response = await Http.DeleteAsync(url);
..
I have tried to set url = $"https://localhost:XXXX/delete..... but same result.
Filename are in form "picture.png"
StatusCode: 404, ReasonPhrase: 'Not Found', Version: 1.1, Content:
System.Net.Http.HttpConnectionResponse Content, Headers: { Set-Cookie:
x-ms-gateway-slice=estsfd; path=/; secure; httponly DATE...
I'm a newbie on api controller so I have no clue what I missed. Don't even know where to start google...
[EDIT : Added Swagger to project]
After analysed with Swagger, I got this in swagger :
[DELETE] /delete/{filename}
Added a filename and execute, got this requested url :
https://localhost:7285/delete/_eskilssk%C3%A4rmklipp.PNG
And the file are deleted. So far so good.
Change / added code to this :
string filename = WebUtility.UrlEncode(fil.Namn);
string baseUrl = $"https://localhost:7285/delete/{filename}";
await JsRuntime.ToastrSuccess("Info : " + baseUrl);
HttpResponseMessage response = await Http.DeleteAsync(baseUrl);
My Toastr gives me :
https://localhost:7285/delete/_eskilssk%C3%A4rmklipp.PNG
same as swagger...
But this in my output i Visual studio :
System.Net.Http.HttpClient.Default.LogicalHandler: Information: Start
processing HTTP request DELETE
https://localhost:7285/delete/_eskilsskärmklipp.PNG
System.Net.Http.HttpClient.Default.ClientHandler: Information: Sending
HTTP request DELETE
https://localhost:7285/delete/_eskilsskärmklipp.PNG
Could it be my encoding that's wrong?
My Program.cs, maybe wrong order?
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
options.AddPolicy("Admin", policy => policy.RequireClaim("role", "Admin"));
});
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
}, ServiceLifetime.Transient);
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.AddScoped<>(); // Some repositories
..
builder.Services.AddScoped<DialogService>();
builder.Services.AddScoped<NotificationService>();
builder.Services.AddScoped<TooltipService>();
builder.Services.AddScoped<ContextMenuService>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
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();
}
app.UseDeveloperExceptionPage(); // Remove when publish!!!
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.MapDefaultControllerRoute();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Blazor API V1");
});
app.Run();
2022-11-10
Problem found, when I comment out // app.UseAuthentication and app.UseAuthorization I could reach the API from my component. It's a security problem and doesn't have anything to do with this original question.
Start a new question with more correct information.
Blazor server and API in same project, 404 not found when app.UserAuth is activate
In ASP.NET Core, the action's route is : [controller]/[action]. In your case :
/upload/delete/{filename}
The client need to call this url like :
..
#inject HttpClient Http
..
string url = $"upload/delete/{filename}"
HttpResponseMessage response = await Http.DeleteAsync(url);
..
If you want the action's url is delete/{filename}, then you can start the action route segment by /. ASP.NET Core MVC will ignore the controller route segment when the action route segment start by / like :
[HttpDelete("/delete/{filename}")]
public IActionResult Delete(string filename)

ASP.NET Core 3.0 Redirect HTTP 4XX and 5XX requests to customized error pages while keeping the error code

I'm looking to redirect HTTP requests with 4XX or 5XX error code to a custom error page, while keeping the error code at the request level. I also want to redirect exceptions to a custom error page, with an error code 500.
For that I used in my Startup file
"app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseExceptionHandler("/error/500");"
associated with an Error controller.
The part about exceptions works well.
I also manage to redirect non-existent routes to my custom page while keeping the 404 error.
However, I can't redirect the following actions to my custom error pages:
return NotFound()
return BadRequest()
return StatusCode(404)
What would be the technical solution applied to accomplish this?
Here is the Configure function of my Startup file :
app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseExceptionHandler("/error/500");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "Error-StatusCode-Route",
pattern: "error/{statusCode}",
defaults: new { controller = "Error", action = "InternalServerError" }
);
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
You could custom middleware to deal with it:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseExceptionHandler("/error/500");
app.Use(async (context, next) =>
{
await next();
var code = context.Response.StatusCode;
var newPath = new PathString("/error/"+code);
var originalPath = context.Request.Path;
var originalQueryString = context.Request.QueryString;
context.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()
{
OriginalPathBase = context.Request.PathBase.Value,
OriginalPath = originalPath.Value,
OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
});
// An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset
// the endpoint and route values to ensure things are re-calculated.
context.SetEndpoint(endpoint: null);
var routeValuesFeature = context.Features.Get<IRouteValuesFeature>();
routeValuesFeature?.RouteValues?.Clear();
context.Request.Path = newPath;
try
{
await next();
}
finally
{
context.Request.QueryString = originalQueryString;
context.Request.Path = originalPath;
context.Features.Set<IStatusCodeReExecuteFeature>(null);
}
});
app.UseHttpsRedirection();
app.UseStaticFiles();
//...
}
For your ErrorController:
public class ErrorController : Controller
{
// GET: /<controller>/
public IActionResult InternalServerError()
{
return View();
}
[Route("error/404")]
public IActionResult StatusCode404()
{
//redirect to the StatusCode404.cshtml
return View();
}
[Route("error/400")]
public IActionResult StatusCode400()
{
return View();
}
}
If you are using core3, then this is a known bug. This bug will be fixed in 3.1.
Here is a link to the issue: https://github.com/aspnet/AspNetCore/issues/13715
For now there is a workaround. You can add this code right after you call app.UseStatusCodePagesWithReExecute("/error/{0}");
app.Use((context, next) =>
{
context.SetEndpoint(null);
return next();
});
This will render your custom pages when you return NotFound or BadRequest from your controller action.

ASP.net-core 3.0 - Is it possible to return custom error page when user is not in a policy?

I'm creating an intranet website and I'm having some trouble with the authentication part. I would like to limit the access for a controller to users in a specific Active Directory Roles. If the user is not in the specified Roles, then it should redirect him to a custom error page.
Windows authentication is enabled. I've tried the following solutions :
I created a custom policy in my ConfigureServices method inside my Startup.cs :
...
services.AddAuthorization(options =>
{
options.AddPolicy("ADRoleOnly", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole(Configuration["SecuritySettings:ADGroup"], Configuration["SecuritySettings:AdminGroup"]);
});
});
services.AddAuthentication(IISDefaults.AuthenticationScheme);
....
with inside my appsettings.json my active directory groups (not the one i'm really using of course) :
"SecuritySettings": {
"ADGroup": "MyDomain\\MyADGroup",
"AdminGroup": "MyDomain\\MyAdminGroup"
}}
and inside my Configure method :
...
app.UseAuthorization();
app.UseAuthentication();
app.UseStatusCodePagesWithReExecute("/Home/ErrorCode/{0}");
...
I have the following controller :
[Area("CRUD")]
[Authorize(Policy = "ADRoleOnly")]
public class MyController : Controller
I have a HomeController with the following method :
[AllowAnonymous]
public IActionResult ErrorCode(string id)
{
return View();
}
but when I debug my site, this method is never reached.
If I'm a user inside one of the specified roles of my policy, it's all working as expected.
But if I'm not a member of the roles, I'm redirected to the default navigator page.
I would like to redirect to a custom error page. I thought that was the purpose of
app.UseStatusCodePagesWithReExecute("/Home/ErrorCode/{0}");
It will generate a 403 statuscode when the policy fails,app.UseStatusCodePagesWithReExecute does not detect 403:
UseStatusCodePagesWithReExecute is not working for forbidden (403)
You could write a custom middleware to deal with it :
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/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.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode == 403)
{
var newPath = new PathString("/Home/ErrorCode/403");
var originalPath = context.Request.Path;
var originalQueryString = context.Request.QueryString;
context.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()
{
OriginalPathBase = context.Request.PathBase.Value,
OriginalPath = originalPath.Value,
OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
});
// An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset
// the endpoint and route values to ensure things are re-calculated.
context.SetEndpoint(endpoint: null);
var routeValuesFeature = context.Features.Get<IRouteValuesFeature>();
routeValuesFeature?.RouteValues?.Clear();
context.Request.Path = newPath;
try
{
await next();
}
finally
{
context.Request.QueryString = originalQueryString;
context.Request.Path = originalPath;
context.Features.Set<IStatusCodeReExecuteFeature>(null);
}
// which policy failed? need to inform consumer which requirement was not met
//await next();
}
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}

How to cache static files in ASP.NET Core?

I can't seem to enable caching of static files in ASP.NET Core 2.2. I have the following in my Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseCors(...);
}
else {
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseSignalR(routes => { routes.MapHub<NotifyHub>("/..."); });
app.UseResponseCompression();
app.UseStaticFiles();
app.UseSpaStaticFiles(new StaticFileOptions() {
OnPrepareResponse = (ctx) => {
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public, max-age=31557600"; // cache for 1 year
}
});
app.UseMvc();
app.UseSpa(spa => {
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment()) {
spa.UseVueCli(npmScript: "serve", port: 8080);
}
});
}
When I try and Audit the production site on HTTPS using chrome I keep getting "Serve static assets with an efficient cache policy":
In the network tab there is no mention of caching in the headers, when I press F5 it seems everything is served from disk cache. But, how can I be sure my caching setting is working if the audit is showing its not?
This is working in ASP.NET Core 2.2 to 3.1:
I know this is a bit similar to Fredrik's answer but you don't have to type literal strings in order to get the cache control header
app.UseStaticFiles(new StaticFileOptions()
{
HttpsCompression = Microsoft.AspNetCore.Http.Features.HttpsCompressionMode.Compress,
OnPrepareResponse = (context) =>
{
var headers = context.Context.Response.GetTypedHeaders();
headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(30)
};
}
});
I do not know what UseSpaStaticFiles is but you can add cache options in UseStaticFiles. You have missed to set an Expires header.
// Use static files
app.UseStaticFiles(new StaticFileOptions {
OnPrepareResponse = ctx =>
{
// Cache static files for 30 days
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000");
ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddDays(30).ToString("R", CultureInfo.InvariantCulture));
}
});
Beware that you also need a way to invalidate cache when you make changes to static files.
I have written a blog post about this: Minify and cache static files in ASP.NET Core

Client Side Deep Links with WebpackDevMiddleware 404s

I am using the WebpackDevMiddleware for Development builds to serve up a Vue.js application that uses client-side routing. The SPA application is served up from the root url just fine, but if I attempt to use any client-side deep links, I get a 404.
Note running as Production works as expected.
What I want:
http://locahost/ - serve up the vue app.
http://localhost/overlays/chat - serve up the vue app.
http://localhost/api/* - serve up the api routes handled server side.
There is a minimum viable reproduction of the problem in this repository. You can run it using vscode debugging as Development environment where the bug happens. There is also a script /scripts/local-production that will build and run as Production environment, where it works as expected.
Relevant portions of my Startup.cs looks like this:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// In production, the Vue files will be served
// from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = Configuration["Client"];
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//set up default mvc routing
app.UseMvc(routes =>
{
routes.MapRoute("default", "api/{controller=Home}/{action=Index}/{id?}");
});
//setup spa routing for both dev and prod
if (env.IsDevelopment())
{
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true,
ProjectPath = Path.Combine(env.ContentRootPath, Configuration["ClientProjectPath"]),
ConfigFile = Path.Combine(env.ContentRootPath, Configuration["ClientProjectConfigPath"])
});
}
else
{
app.UseWhen(context => !context.Request.Path.Value.StartsWith("/api"),
builder => {
app.UseSpaStaticFiles();
app.UseSpa(spa => {
spa.Options.DefaultPage = "/index.html";
});
app.UseMvc(routes => {
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Fallback", action = "Index" });
});
});
}
}
}
I was able to get around this using the status code pages middleware to handle all status codes and re-execute using the root path. This will cause the spa app to be served up for all status codes in the 400-599 range which is not quite what I want but gets me working again at least.
//setup spa routing for both dev and prod
if (env.IsDevelopment())
{
//force client side deep links to render the spa on 404s
app.UseStatusCodePagesWithReExecute("/");
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true,
ProjectPath = Path.Combine(env.ContentRootPath, Configuration["ClientProjectPath"]),
ConfigFile = Path.Combine(env.ContentRootPath, Configuration["ClientProjectConfigPath"])
});
}
Hopefully, this will help someone in the future that might be bumping up against this issue.