asp.net core check route attribute in middleware - asp.net-core

I'm trying to build some ASP.Net core middleware.
It needs see if the current route is marked as [Authorize].
eg:
public async Task Invoke(HttpContext context)
{
if(context.Request.Path.Value.StartsWith("/api"))
{
// check if route is marked as [Authorize]
// and then do some logic
}
await _next.Invoke(context);
}
Does anyone know how this could be achieved or if it's even possible?
If not, what would be a good alternative approach?

I believe it can be achieved in a middleware class via:
var hasAuthorizeAttribute = context.Features.Get<IEndpointFeature>().Endpoint.Metadata
.Any(m => m is AuthorizeAttribute);

Without knowing exactly what you want to achieve, it's a bit tricky to answer, but I suggest you have a look at the controller filters : https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters
I put together an example:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.AddMvcOptions(options => options.Filters.Insert(0, new CustomFilter()));
}
CustomFilter.cs
public class CustomFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if(context.HttpContext.Request.Path.Value.StartsWith("/api"))
{
var controllerInfo = context.ActionDescriptor as ControllerActionDescriptor;
var hasAuthorizeAttr = controllerInfo.ControllerTypeInfo.CustomAttributes.Any(_ => _.AttributeType == typeof(AuthorizeAttribute));
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// NOP
}
}

HansElsen's answer is correct, but I would like to add some things to note about it:
You can use the extension method context.GetEndpoint() which is exactly the same as context.Features.Get<IEndpointFeature>().Endpoint
The endpoint will always be null if you set up your middleware before calling app.UseRouting() in your Startup.cs:
Don't do:
app.UseMiddleware<MyMiddleware>();
app.UseRouting();
Do:
app.UseRouting();
app.UseMiddleware<MyMiddleware>();

Just self-answering to close the question.
Doesn't seem like it's possible to do this as it's too early in the pipeline.

Related

.NET Core Middleware - access IApplicationBuilder in a controller?

I need to access IApplicationBuilder inside a controller.
What I have tried :-
I have written middleware (app.UseMyMiddleware) as follows
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor httpContextAccessor)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMyMiddleware();
app.UseAuthentication();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
///TODO - Pass IApplicationBuilder to HttpContext
await _next(context);
}
}
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
but I can't figure out how I can pass IApplicationBuilder to HttpContext in Invoke method. so, that I can use it in a controller.
I have also referred following stackoverflow question-answer
how to access IApplicationBuilder in a controller?
.Net Core Middleware - Getting Form Data from Request
Question(s) :-
How can pass IApplicationBuilder to HttpContext in Invoke method to use it in controller?
Is there any better way to access IApplicationBuilder inside controller apart from middleware?
IApplicationBuilder was not designed to work the way you want it to. Instead, if you have some data created at build time that you want to be available to middleware add a Singleton to the services and inject the singleton into the middleware.
You cannot access IApplicationBuilder anywhere later after completing the application building phase (after running Configure method). It's not available for injection at all.
However for the purpose of plugging-in or configuring middlewares at runtime based on request data (from HttpContext), you can use .UseWhen. Another one for terminal middleware is .MapWhen but I think that's not for your case. Here is an example of .UseWhen:
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
var allOptions = new [] {"option 1","option 2"};
foreach(var option in allOptions){
var currentOption = option;
builder.UseWhen(context => {
//suppose you can get the user's selected option from query string
var selectedOption = context.Request.Query["option_key"];
return selectedOption == currentOption;
}, app => {
//your MyMiddleware is supposed to accept one argument
app.UseMiddleware<MyMiddleware>(currentOption);
});
}
return builder;
}
}
To simplify it I suppose your options are just strings, you must know beforehand all possible options that the user can select via UI. Each one will be an exact match for the condition to plug-in a middleware and they must be all exclusive (so just one of them can enable one corresponding middleware), otherwise there will be duplicate middlewares, which may cause some issue.
By expressing the foreach above more clearly, it may represent something as follows:
//kind of pseudo code
if(selectedOption1){
app.UseMiddleware<MyMiddleware>("option 1");
} else if(selectedOption2){
app.UseMiddleware<MyMiddleware>("option 2");
}
...
You must decide how you get the selected option from the user (in the example above I get it from query string). You can get it from Cookie as well (to remember the user's selection) or from other sources such as route data, headers, form, request body. I think that's another issue, so if you have problem with that, please ask in another question.
First up all thanks to #Kingking and #GlennSills for there solution and valuable comments.
I have solved this problem as
Created one class which inherit from Hangfire.JobStorage as follows
public class HangfireSqlServerStorageExtension : Hangfire.JobStorage
{
private readonly HangfireSqlServerStorage _hangfireSqlServerStorage = new HangfireSqlServerStorage();
public HangfireSqlServerStorageExtension(string nameOrConnectionString)
{
_hangfireSqlServerStorage.SqlServerStorageOptions = new SqlServerStorageOptions();
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public HangfireSqlServerStorageExtension(string nameOrConnectionString, SqlServerStorageOptions options)
{
_hangfireSqlServerStorage.SqlServerStorageOptions = options;
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public void UpdateConnectionString(string nameOrConnectionString)
{
_hangfireSqlServerStorage.SqlServerStorage = new SqlServerStorage(nameOrConnectionString, _hangfireSqlServerStorage.SqlServerStorageOptions);
}
public override IStorageConnection GetConnection()
{
return _hangfireSqlServerStorage.SqlServerStorage.GetConnection();
}
public override IMonitoringApi GetMonitoringApi()
{
return _hangfireSqlServerStorage.SqlServerStorage.GetMonitoringApi();
}
}
HangfireSqlServerStorage.cs
Used in HangfireSqlServerStorageExtension class above
public class HangfireSqlServerStorage
{
public SqlServerStorage SqlServerStorage { get; set; }
public SqlServerStorageOptions SqlServerStorageOptions { get; set; }
}
Startup.cs
In Startup file add singleton service for HangfireSqlServerStorageExtension instance and configure hangfire dashboard as follows
public class Startup
{
///Other necessary code here
public static HangfireSqlServerStorageExtension HangfireSqlServerStorageExtension { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
///Other necessary code here
HangfireSqlServerStorageExtension = new HangfireSqlServerStorageExtension("DBConnecttionString"));
services.AddSingleton<HangfireSqlServerStorageExtension>(HangfireSqlServerStorageExtension);
services.AddHangfire(configuration => configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_170));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor httpContextAccessor)
{
//Other necessary code here
app.UseHangfireDashboard("/Dashboard", new DashboardOptions(), HangfireSqlServerStorageExtension);
//Other necessary code here
}
}
Inside controller I have used it as follows
HangfireController.cs
public class HangfireController : Controller
{
protected readonly HangfireSqlServerStorageExtension
hangfireSqlServerStorageExtension;
public HangfireController(HangfireSqlServerStorageExtension hangfireSqlServerStorageExtension)
{
this.hangfireSqlServerStorageExtension = hangfireSqlServerStorageExtension;
}
public IActionResult DisplayHangfireDashboard()
{
// Update connString as follows
hangfireSqlServerStorageExtension.UpdateConnectionString(connString);
var hangfireDashboardUrl = $"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}" + "/Dashboard";
return Json(new { url = hangfireDashboardUrl });
}
}

Generic string router with DB in Asp.net Core

I am creating an internet store. And I want to add short URLs for products, categories and so on.
For example:
store.com/iphone-7-plus
This link should open the page with iPhone 7 plus product.
The logic is:
The server receives an URL
The server try it against existent routes
If there is no any route for this path - the server looks at a DB and try to find a product or category with such title.
Obvious solutions and why are they not applicable:
The first solution is a new route like that:
public class StringRouter : IRouter
{
private readonly IRouter _defaultRouter;
public StringRouter(IRouter defaultRouter)
{
_defaultRouter = defaultRouter;
}
public async Task RouteAsync(RouteContext context)
{
// special loggic
await _defaultRouter.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaultRouter.GetVirtualPath(context);
}
}
The problem is I can't provide any access to my DB from StringRouter.
The second solution is:
public class MasterController : Controller
{
[Route("{path}")]
public IActionResult Map(string path)
{
// some logic
}
}
The problem is the server receive literally all callings like store.com/robots.txt
So the question is still open - could you please advise me some applicable solution?
For accessing DbContext, you could try :
using Microsoft.Extensions.DependencyInjection;
public async Task RouteAsync(RouteContext context)
{
var dbContext = context.HttpContext.RequestServices.GetRequiredService<RouterProContext>();
var products = dbContext.Product.ToList();
await _defaultRouter.RouteAsync(context);
}
You also could try Middleware to check whether the reuqest is not exist, and then return the expected response.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
{
app.Use(async (context,next) => {
await next.Invoke();
// add your own business logic to check this if statement
if (context.Response.StatusCode == 404)
{
var db = context.RequestServices.GetRequiredService<RouterProContext>();
var users = db.Users.ToList();
await context.Response.WriteAsync("Request From Middleware");
}
});
//your rest code
}

Set dummy IP address in integration test with Asp.Net Core TestServer

I have a C# Asp.Net Core (1.x) project, implementing a web REST API, and its related integration test project, where before any test there's a setup similar to:
// ...
IWebHostBuilder webHostBuilder = GetWebHostBuilderSimilarToRealOne()
.UseStartup<MyTestStartup>();
TestServer server = new TestServer(webHostBuilder);
server.BaseAddress = new Uri("http://localhost:5000");
HttpClient client = server.CreateClient();
// ...
During tests, the client is used to send HTTP requests to web API (the system under test) and retrieve responses.
Within actual system under test there's some component extracting sender IP address from each request, as in:
HttpContext httpContext = ReceiveHttpContextDuringAuthentication();
// edge cases omitted for brevity
string remoteIpAddress = httpContext?.Connection?.RemoteIpAddress?.ToString()
Now during integration tests this bit of code fails to find an IP address, as RemoteIpAddress is always null.
Is there a way to set that to some known value from within test code? I searched here on SO but could not find anything similar. TA
You can write middleware to set custom IP Address since this property is writable:
public class FakeRemoteIpAddressMiddleware
{
private readonly RequestDelegate next;
private readonly IPAddress fakeIpAddress = IPAddress.Parse("127.168.1.32");
public FakeRemoteIpAddressMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext httpContext)
{
httpContext.Connection.RemoteIpAddress = fakeIpAddress;
await this.next(httpContext);
}
}
Then you can create StartupStub class like this:
public class StartupStub : Startup
{
public StartupStub(IConfiguration configuration) : base(configuration)
{
}
public override void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<FakeRemoteIpAddressMiddleware>();
base.Configure(app, env);
}
}
And use it to create a TestServer:
new TestServer(new WebHostBuilder().UseStartup<StartupStub>());
As per this answer in ASP.NET Core, is there any way to set up middleware from Program.cs?
It's also possible to configure the middleware from ConfigureServices, which allows you to create a custom WebApplicationFactory without the need for a StartupStub class:
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost
.CreateDefaultBuilder<Startup>(new string[0])
.ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, CustomStartupFilter>();
});
}
}
public class CustomStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.UseMiddleware<FakeRemoteIpAddressMiddleware>();
next(app);
};
}
}
Using WebHost.CreateDefaultBuilder can mess up with your app configuration.
And there's no need to change Product code just to accommodate for testing, unless absolutely necessary.
The simplest way to add your own middleware, without overriding Startup class methods, is to add the middleware through a IStartupFilterā€ as suggested by Elliott's answer.
But instead of using WebHost.CreateDefaultBuilder, just use
base.CreateWebHostBuilder().ConfigureServices...
public class CustomWAF : WebApplicationFactory<Startup>
{
protected override IWebHostBuilder CreateWebHostBuilder()
{
return base.CreateWebHostBuilder().ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, CustomStartupFilter>();
});
}
}
I used Elliott's answer within an ASP.NET Core 2.2 project. However, updating to ASP.NET 5.0, I had to replace the override of CreateWebHostBuilder with the below override of CreateHostBuilder:
protected override IHostBuilder CreateHostBuilder()
{
return Host
.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
})
.ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, CustomStartupFilter>();
});
}

Wrapping results of ASP.NET Core WebAPI methods using IResultFilter

I have implemented a result filter like this:
public class ResultWrapperFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.ActionDescriptor is ControllerActionDescriptor))
{
return;
}
var objectResult = context.Result as ObjectResult;
if (objectResult == null)
{
return;
}
if (!(objectResult.Value is WrappedResponseBase))
{
objectResult.Value = new WrappedResponse(objectResult.Value);
}
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
}
The filter is used by configuring MvcOptions through ConfigureServices(IServiceCollection services) like this:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MvcOptions>(
options => { options.Filters.AddService<ResultWrapperFilter>(); });
services.AddMvc();
// ... the rest is omitted for readability
}
The problem I'm experiencing is this filter is causing InvalidCastException: Unable to cast object of type 'WrappedResponse' to type 'System.String' (the method in question has string as the return value type).
Am I even allowed to do this using IResultFilter?
NOTE: I am aware of the possibility of using middleware to accomplish the response wrapping. I don't want to use the middleware to accomplish this because the middleware doesn't have access to context.Result as ObjectResult. Deserializing from the response stream, wrapping and serializing again seems so unnecessary.
An answer just came to me.
When setting objectResult.Value, objectResult.DeclaredType also needs to be set.
So in this case:
if (!(objectResult.Value is WrappedResponseBase))
{
objectResult.Value = new WrappedResponse(objectResult.Value);
objectResult.DeclaredType = typeof(WrappedResponse);
}

Conditionally use custom middleware

I created my custom authentication middleware in asp. net core project, and registered it as shown below:
public class MyAuthenticationMidleware
{
private readonly RequestDelegate _next;
public ConnectAuthenticationMidleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (!UserIsAuthenticated())
{
context.Response.StatusCode = 401;
return;
}
...
await _next.Invoke(context);
}
}
public static class MyAuthenticationMidlewareExtensions
{
public static IApplicationBuilder UseMyAuthentication(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyAuthenticationMidleware>();
}
}
In Startup:
public void Configure(...)
{
app.UseStaticFiles();
app.UseMyAuthentication();
app.UseMvc();
}
This works correctly - authentication middleware is run for each request. If user is not authenticated, 401 is returned. Otherwise specific mvc action is invoked.
What I tried to do was to prevent the authentication middleware from running for some specific actions. I used MapWhen method to create another extension method and used it as follows:
public static class MyAuthenticationMidlewareExtensions
{
public static IApplicationBuilder UseMyAuthentication(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyAuthenticationMidleware>();
}
public static IApplicationBuilder UseMyAuthenticationWhen(this IApplicationBuilder builder, Func<HttpContext, bool> predicate)
{
return builder.MapWhen(predicate, applicationBuilder => applicationBuilder.UseMyAuthentication());
}
}
public void Configure(...)
{
app.UseStaticFiles();
app.UseMyAuthenticationWhen(context => context.Request.Path != "xyz");
app.UseMvc();
}
Unfortunately, this doesn't work as expected. The middleware is invoked only when path is different than "xyz", but it seems that it short-circuts the whole chain - no mvc specific actions or filters are invoked.
Probably my understanding of MapWhen is incorrect. Is there any way to get the result I want?
MapWhen creates a new pipeline branch when the supplied predicate is true, and that branch does not rejoin with the main branch where you have UseMvc().
You can change your extension method to use UseWhen instead of MapWhen. UseWhen rejoins with the main pipeline so that your UseMvc() will still get called.
Note: While the above link references aspnet-contrib, the UseWhen extension method is now part of Microsoft.AspNetCore.Http.Abstractions.
This allows you to keep UseMvc() explicit in your Configure method instead of hidden away in your authentication extension method, where it really has no business being.
MapWhen is used to seperate middleware pipeline. If you want to use mvc for branced pipeline you need to add separetely. So you should use .UseMvc(); in extension method like below:
public static IApplicationBuilder UseMyAuthenticationWhen(this IApplicationBuilder builder, Func<HttpContext, bool> predicate)
{
return builder.MapWhen(predicate, applicationBuilder =>
{
applicationBuilder.UseMyAuthentication();
applicationBuilder.UseMvc();
});
}
However i wouldn't go with your way. For authentication middleware i would implement my own middleware like Simple token based authentication/authorization in asp.net core for Mongodb datastore and use Authorize attribute for authorization mvc actions.