I am new the API in general, let me give you the background of the API and what I want it to do.
I have a API have that are external facing and so every incoming request are required to check the signature from header. literality my code in every controller call are checking the signature and created many duplicated code.
my question is how can reduces those duplicated code ? do I use Custom Attributes, or AuthorizeAttribute
here are some of the example code:
[Route("[controller]")]
[ApiController]
public class ExampleController : ControllerBase
{
public async Task<Result> Call_1(Rquest request)
{
string signaturel;
signature = Util.getHeaderSignature(request);
if(unit.IsSinatureValid(signaturel, someVar1, someVar2))
{
(My logic)
}
else{ return "InvalidSinaturemessage" }
}
public async Task<Result> Call_2(Rquest request)
{
string signaturel;
signature = Util.getHeaderSignature(request);
if(unit.IsSinatureValid(signaturel, someVar1, someVar2))
{
(My logic)
}
else{ return "InvalidSinaturemessage" }
}
}
above code is just for showing, the actual Sinature checking logic is around 20 lines of code on every single controller method.
Yes, you can do that using action filters. It's described in documentation
Put your code for checking into OnActionExecuting method. So, you can write Result in the action filter if the signature isn't valid.
In case you need specific result structure you can create your own ObjectResult:
public class ForbiddenObjectResult : ObjectResult
{
public string Message { get; private set; }
public ForbiddenObjectResult(object value, string message)
: base(value)
{
StatusCode = StatusCodes.Status403Forbidden;
Message = message;
}
}
...
string signaturel;
signature = Util.getHeaderSignature(context.HttpContext.Request);
if(!unit.IsSinatureValid(signaturel, someVar1, someVar2))
{
context.Result = new ForbiddenObjectResult(filterContext.ModelState, "InvalidSinaturemessage");
}
And to register it for all your endpoints(if needed):
services.AddControllersWithViews(options =>
{
options.Filters.Add<YourActionFilter>();
});
You can use token based authentication or filter method. For reference
Token based authentication
Custom Filter
Related
In few places in legacy code (more than 100 controllers), we are running action from other controllers.
In .NET Framework it runs OK - ClaimsPrincipal in both controller's action have correct values, but in .NET Core, running SecondController.internalPut() from FirstController gives me NullReferenceException.
FirstController:
[EnableCors]
public class FirstController : BaseApiController
{
public FirstController(IContextFactory contextFactory) : base(contextFactory)
{
}
[HttpPut]
[HttpPost]
[Route("/api/firstcontroller")]
public IActionResult Put([FromBody] MyDTO data)
{
var token = Identity.Token; // <--- correct value
var secondController = new SecondController(ContextFactory);
secondController.internalPut(something); <--- NullReferenceException
return Ok();
}
}
SecondController:
[EnableCors]
public class SecondController : BaseApiController
{
public SecondController(IContextFactory contextFactory) : base(contextFactory)
{
}
[HttpPut]
[HttpPost]
public async Task<IActionResult> Put(Guid myGuid)
{
internalPut(something); // <-- OK
return Ok();
}
internal void internalPut(object something)
{
var token = Identity.Token; // <--- NullReferenceException when running from FirstController!!
}
}
And BaseApiController with TokenIdentity:
[ApiController]
[Route("/api/[controller]")]
[Route("/api/[controller]/[action]")]
public class BaseApiController : ControllerBase
{
protected readonly IMyContextFactory ContextFactory;
public BaseApiController(IMyContextFactory contextFactory)
{
ContextFactory = contextFactory;
}
public TokenIdentity Identity => User?.Identity as TokenIdentity;
}
public class TokenIdentity : GenericIdentity
{
public Guid Token { get; set; }
public string User { get; set; }
public TokenIdentity(Guid token) : base(token.ToString())
{
Token = token;
}
}
How is the easiest fix for this bug? I know that I can change BaseApiController implementation to get ClaimsPrincipal from IHttpContextAccessor, but this means that I need to update constructors for all > 100 controllers in code...
It is another way to always have ClaimsPrincipal when we are calling action from another controller?
What I recommend as the correct solution
I can't emphasise enough how much I recommend moving shared functionality into its own services, or perhaps look at using the Mediator Pattern (e.g. using the MediatR library) to decouple your controllers from their functionality a little. What I provide below is not a solution, but a band-aid.
What I recommend a QUICK FIX only
Why is this only a quick fix?: because this doesn't instantiate the correct action details and route parameters, so it could potentially cause you some hard-to-find bugs, weird behaviour, URLs maybe not generating correctly (if you use this), etc.
Why am I recommending it?: because I know that sometimes time is not on our side and that perhaps you need a quick fix to get this working while you work on a better solution.
Hacky quick fix
You could add the following method to your base controller class:
private TController CreateController<TController>() where TController: ControllerBase
{
var actionDescriptor = new ControllerActionDescriptor()
{
ControllerTypeInfo = typeof(TController).GetTypeInfo()
};
var controllerFactory = this.HttpContext.RequestServices.GetRequiredService<IControllerFactoryProvider>().CreateControllerFactory(actionDescriptor);
return controllerFactory(this.ControllerContext) as TController;
}
Then instead of var secondController = new SecondController(ContextFactory); you would write:
var secondController = CreateController<SecondController>();
I have a simple controller method like this:
public IEnumerable<IEntity> GetEntities(ParamsModel args)
{
//set break point here to examine the args
return null;
}
And here is my ParamsModel:
public class ParamsModel {
public string Test;
}
And here is my client's method to send get request:
//_client here is an instance of RestClient
public async Task<IEnumerable<T>> GetEntitiesAsync()
{
var request = new RestRequest("somePath");
var o = new {
Test = "OK"
};
request.AddJsonBody(o);
return await _client.GetAsync<List<T>>(request);
}
After running the method GetEntitiesAsync, the break point (in the controller's method) is hit. However the args is null, really?
I've also tried the following:
public async Task<IEnumerable<T>> GetEntitiesAsync()
{
var request = new RestRequest("somePath");
request.AddParameter("Test", "OK");
return await _client.GetAsync<List<T>>(request);
}
However that did not work as well (args is null in the controller's method).
If I change the controller's method to something like this (and use the client code as right above), I can see the single simple argument of string has value parsed OK ("OK") inside the controller's method:
public IEnumerable<IEntity> GetEntities(string Test)
{
//here we can see that Test has value of "OK"
return null;
}
Really I don't understand what's wrong with my code.
Actually I worked with RestSharp at least a year ago but now it seems to have some new methods (such as the GetAsync as I used in my code), as before I used the Execute and ExecuteAsync.
Could you spot anything wrong here? Thanks!
PS: I'm using RestSharp 106.6.7
Update action to state explicitly where to look for and bind data using [FromUri]
public IHttpActionResult GetEntities([FromUri]ParamsModel args) {
//...
return Ok(entities);
}
To force Web API to read a complex type from the URI, add the [FromUri] attribute to the parameter.
Reference Parameter Binding in ASP.NET Web API
The example with AddParameter
public async Task<IEnumerable<T>> GetEntitiesAsync() {
var request = new RestRequest("somePath");
request.AddParameter("Test", "OK");
return await _client.GetAsync<List<T>>(request);
}
Should work now.
Note that the model should use properties instead of fields
public class ParamsModel {
public string Test { get; set; }
}
I have a logic to apply in case the request received is a BadRequest, to do this I have created a filter:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
// Apply logic
}
}
}
In Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => { options.Filters.Add<ValidateModelAttribute>(); });
}
Controller:
[Route("api/[controller]")]
[ApiController]
public class VerifyController : ControllerBase
{
[Route("test")]
[HttpPost]
[ValidateModel]
public ActionResult<Guid> validationTest(PersonalInfo personalInfo)
{
return null;
}
}
Model:
public class PersonalInfo
{
public string FirstName { get; set; }
[RegularExpression("\\d{4}-?\\d{2}-?\\d{2}", ErrorMessage = "Date must be properly formatted according to ISO 8601")]
public string BirthDate { get; set; }
}
The thing is when I put a break point on the line:
if (!context.ModelState.IsValid)
execution reaches this line only if the request I send is valid. Why it is not passing the filter if I send a bad request?
The [ApiController] attribute that you've applied to your controller adds Automatic HTTP 400 Responses to the MVC pipeline, which means that your custom filter and action aren't executed if ModelState is invalid.
I see a few options for affecting how this works:
Remove the [ApiController] attribute
Although you can just remove the [ApiController] attribute, this would also cause the loss of some of the other features it provides, such as Binding source parameter inference.
Disable only the Automatic HTTP 400 Responses
Here's an example from the docs that shows how to disable just this feature:
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// ...
options.SuppressModelStateInvalidFilter = true;
// ...
}
This code goes inside of your Startup's ConfigureServices method.
Customise the automatic response that gets generated
If you just want to provide a custom response to the caller, you can customise what gets returned. I've already described how this works in another answer, here.
An example of intersection for logging is describe in Log automatic 400 responses
Add configuration in Startup.ConfigureServices.
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// To preserve the default behavior, capture the original delegate to call later.
var builtInFactory = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Startup>>();
// Perform logging here.
//E.g. logger.LogError($”{context.ModelState}”);
logger.LogWarning(context.ModelState.ModelStateErrorsToString());
// Invoke the default behavior, which produces a ValidationProblemDetails response.
// To produce a custom response, return a different implementation of IActionResult instead.
return builtInFactory(context);
};
});
public static String ModelStateErrorsToString(this ModelStateDictionary modelState)
{
IEnumerable<ModelError> allErrors = modelState.Values.SelectMany(v => v.Errors);
StringBuilder sb = new StringBuilder();
foreach (ModelError error in allErrors)
{
sb.AppendLine($"error {error.ErrorMessage} {error.Exception}");
}
return sb.ToString();
}
As the attribute filter in the life cycle of the .Net Core you can’t handle it. The filter layer with ModelState will run after the model binding.
You can handle it with .Net Core middleware as the following https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1&tabs=aspnetcore2x
If you want to SuppressModelStateInvalidFilter on individual action, consider to use custom attribute suggested on https://learn.microsoft.com/en-us/answers/questions/297568/how-to-suppress-suppressmodelstateinvalidfilter-at.html. (And similar answer https://github.com/aspnet/Mvc/issues/8575)
public class SuppressModelStateInvalidFilterAttribute : Attribute, IActionModelConvention
{
private const string FilterTypeName = "ModelStateInvalidFilterFactory";
public void Apply(ActionModel action)
{
for (var i = 0; i < action.Filters.Count; i++)
{
//if (action.Filters[i] is ModelStateInvalidFilter)
if (action.Filters[i].GetType().Name == FilterTypeName)
{
action.Filters.RemoveAt(i);
break;
}
}
}
}
Example of use
[ApiController]
public class PersonController
{
[SuppressModelStateInvalidFilter]
public ActionResult<Person> Get() => new Person();
}
I am trying to determine the correct method to inject a dependency into a controller where the concrete type to be injected is a variable based on a route data parameter.
So far I have the following set up which works perfectly for normal requests:
Controller
public class OrdersController : ODataController
{
private IOrderService ErpService { get; }
public OrdersController(IOrderService orderService)
{
ErpService = orderService;
}
[EnableQuery(PageSize = 100)]
public IQueryable<OrderDto> Get(ODataQueryOptions<OrderDto> queryOptions)
{
return ErpService.Orders(queryOptions);
}
...
// Post
// Patch/Put
// Delete
}
With the following OData route config, I can specify the route template should include a 'company' parameter:
Config
config.MapODataServiceRoute( "ODataRoute", "data/{company}", model, new DefaultODataPathHandler(),
conventions, new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
This allows me to have a static method to read the company ID from the URL:
public static string GetSalesCompanyFromRequest()
{
var salesCompany = "";
if (HttpContext.Current == null) return "";
var routeData = HttpContext.Current.Request.RequestContext.RouteData;
if (routeData.Values.ContainsKey("company"))
{
salesCompany = routeData.Values["company"].ToString();
}
return salesCompany;
}
Then, using Ninject, I can chose which concrete instance of IOrderService to use (simplified for brevity):
kernel.Bind<IOrderService>()
.To<SageOrderService>()
.When(ctx => GetSalesCompanyFromRequest() == "101").InRequestScope();
kernel.Bind<IOrderService>()
.To<DynamicsAxOrderService>()
.When(ctx => GetSalesCompanyFromRequest() == "222").InRequestScope();
kernel.Bind<IOrderService>()
.To<SapOrderService>()
.When(ctx => GetSalesCompanyFromRequest() == "333").InRequestScope();
Connector Config
Id ErpType ConnectionString
--------------------------------------------
111 Sage "connectionstring1"
222 DynamicsAx "connectionstring2"
333 SAP "connectionstring3"
So here's how the following URLs get processed:
http://odata-demo/data/101/Orders
Creates and injects a SageOrderService into OrdersController
http://odata-demo/data/222/Orders
Creates and injects a DynamicsAxOrderService into OrdersController
The same logic applies to many different services, like:
SageStockService/AxStockService
SageBomService/AxBomService
etc
Note:
I chose to put the company Id in the URL so I could configure a reverse proxy to forward requests to a local web server closer to the target database.
This all works perfectly until I try to use OData Batching.
It seems then there is no HttpContext.Current (it is null) when I send a batched request.
This question asks something similar but does not account for OData batched requests.
Comments in this answer suggest injection by route data is code smell but does not elaborate.
So, the question is, how to I get HttpContext.Current for batched OData requests? Or Is there a better way to do what I'm trying to do?
Since the company is already in the route data, I could add an additional company parameter to every single action as follows to allow the company number to be passed in, then use a factory to get the right concrete type:
public class OrdersController : ODataController
{
[EnableQuery(PageSize = 100)]
public IQueryable<OrderDto> Get(ODataQueryOptions<OrderDto> queryOptions, string company)
{
var erpService = ErpServiceFactory.GetService(company);
return erpService.Orders(queryOptions);
}
...
// Post
// Patch/Put
// Delete
}
This means that I would also have to initialise the OrderService within each action which smells a bit.
I suppose this could be less smelly if I used an ActionFilter to locate and pass in the correct concrete type to the action:
public class RequiresOrderServiceAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
string salesCompany = "";
var data = actionContext.Request.GetRouteData();
if (data.Values.ContainsKey("company"))
{
salesCompany = data.Values["company"].ToString();
var orderService = ErpServiceFactory.GetService(company);
actionContext.ActionArguments.Add("erpService", orderService);
}
}
}
public class OrdersController : ODataController
{
[EnableQuery(PageSize = 100)]
public IQueryable<OrderDto> Get(ODataQueryOptions<OrderDto> queryOptions, IOrderService erpService)
{
return erpService.Orders(queryOptions);
}
...
// Post
// Patch/Put
// Delete
}
Thoughts?
I detected a problem in the RequestFilter execution order.
The ValidationFeature in ServiceStack is a Plugin that just registers a Global Request Filter. The Order of Operations points out that Global Request Filters are executed after Filter Attributes with a Priority <0 and before Filter Attributes with a Priority >=0
My BasicAuth filter has -100 priority, and in fact everything goes well if the Service is annotated at class level, but it fails when the annotation is at method level, with the authentication filter being executed after.
I am using 3.9.70
Is there any quick fix for this? Thanks
When you add the annotation at method level then you are creating an Action Request Filter (because you are adding the annotation to an action method) which in the Order of Operations is operation 8, after the other filters have run.
5: Request Filter Attributes with Priority < 0 gets executed
6: Then any Global Request Filters get executed
7: Followed by Request Filter Attributes with Priority >= 0
8: Action Request Filters (New API only)
The best workaround I can suggest is to reconsider your service structure. I imagine you are having these difficulties because you are adding unauthenticated api methods alongside your secure api methods, and thus are using method level attributes to control authentication. So you are presumably doing something like this Your classes and attributes will be different, this is just exemplar:
public class MyService : Service
{
// Unauthenticated API method
public object Get(GetPublicData request)
{
return {};
}
// Secure API method
[MyBasicAuth] // <- Checks user has permission to run this method
public object Get(GetSecureData request)
{
return {};
}
}
I would do this differently, and separate your insecure and secure methods into 2 services. So I use this:
// Wrap in an outer class, then you can still register AppHost with `typeof(MyService).Assembly`
public partial class MyService
{
public class MyPublicService : Service
{
public object Get(GetPublicData request)
{
return {};
}
}
[MyBasicAuth] // <- Check is now class level, can run as expected before Validation
public class MySecureService : Service
{
public object Get(GetSecureData request)
{
return {};
}
}
}
Solution - Deferred Validation:
You can solve your execution order problem by creating your own custom validation feature, which will allow you to defer the validation process. I have created a fully functional self hosted ServiceStack v3 application that demonstrates this.
Full source code here.
Essentially instead of adding the standard ValidationFeature plugin we implement a slightly modified version:
public class MyValidationFeature : IPlugin
{
static readonly ILog Log = LogManager.GetLogger(typeof(MyValidationFeature));
public void Register(IAppHost appHost)
{
// Registers to use your custom validation filter instead of the standard one.
if(!appHost.RequestFilters.Contains(MyValidationFilters.RequestFilter))
appHost.RequestFilters.Add(MyValidationFilters.RequestFilter);
}
}
public static class MyValidationFilters
{
public static void RequestFilter(IHttpRequest req, IHttpResponse res, object requestDto)
{
// Determine if the Request DTO type has a MyRoleAttribute.
// If it does not, run the validation normally. Otherwise defer doing that, it will happen after MyRoleAttribute.
if(!requestDto.GetType().HasAttribute<MyRoleAttribute>()){
Console.WriteLine("Running Validation");
ValidationFilters.RequestFilter(req, res, requestDto);
return;
}
Console.WriteLine("Deferring Validation until Roles are checked");
}
}
Configure to use our plugin:
// Configure to use our custom Validation Feature (MyValidationFeature)
Plugins.Add(new MyValidationFeature());
Then we need to create our custom attribute. Your attribute will be different of course. The key thing you need to do is call ValidationFilters.RequestFilter(req, res, requestDto); if you are satisfied the user has the required role and meets your conditions.
public class MyRoleAttribute : RequestFilterAttribute
{
readonly string[] _roles;
public MyRoleAttribute(params string[] roles)
{
_roles = roles;
}
#region implemented abstract members of RequestFilterAttribute
public override void Execute(IHttpRequest req, IHttpResponse res, object requestDto)
{
Console.WriteLine("Checking for required role");
// Replace with your actual role checking code
var role = req.GetParam("role");
if(role == null || !_roles.Contains(role))
throw HttpError.Unauthorized("You don't have the correct role");
Console.WriteLine("Has required role");
// Perform the deferred validation
Console.WriteLine("Running Validation");
ValidationFilters.RequestFilter(req, res, requestDto);
}
#endregion
}
For this to work we need to apply our custom attribute on the DTO route not the action method. So this will be slightly different to how you are doing it now, but should still be flexible.
[Route("/HaveChristmas", "GET")]
[MyRole("Santa","Rudolph","MrsClaus")] // Notice our custom MyRole attribute.
public class HaveChristmasRequest {}
[Route("/EasterEgg", "GET")]
[MyRole("Easterbunny")]
public class GetEasterEggRequest {}
[Route("/EinsteinsBirthday", "GET")]
public class EinsteinsBirthdayRequest {}
Then your service would look something like this:
public class TestController : Service
{
// Roles: Santa, Rudolph, MrsClaus
public object Get(HaveChristmasRequest request)
{
return new { Presents = "Toy Car, Teddy Bear, Xbox" };
}
// Roles: Easterbunny
public object Get(GetEasterEggRequest request)
{
return new { EasterEgg = "Chocolate" };
}
// No roles required
public object Get(EinsteinsBirthdayRequest request)
{
return new { Birthdate = new DateTime(1879, 3, 14) };
}
}
So when we call the route /EinsteinsBirthday which does not have a MyRole attribute the validation will be called normally, as if using the standard ValidationFeature.
If we call the route /HaveChristmas?role=Santa then our validation plugin will determine that the DTO has our attribute and not run. Then our attribute filter triggers and it will trigger the validation to run. Thus the order is correct.