ValidateAntiForgeryToken in Ajax request with AspNet Core MVC - c#

I have been trying to recreate an Ajax version of the ValidateAntiForgeryToken - there are many blog posts on how to do this for previous versions of MVC, but with the latest MVC 6, none of the code is relevant. The core principle that I am going after, though, is to have the validation look at the Cookie and the Header for the __RequestVerificationToken, instead of comparing the Cookie to a form value. I am using MVC 6.0.0-rc1-final, dnx451 framework, and all of the Microsoft.Extensions libraries are 1.0.0-rc1-final.
My initial thought was to just inherit ValidateAntiForgeryTokenAttribute, but looking at the source code, I would need to return my own implementation of an an Authorization Filter to get it to look at the header.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateAjaxAntiForgeryTokenAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
public int Order { get; set; }
public bool IsReusable => true;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return serviceProvider.GetRequiredService<ValidateAjaxAntiforgeryTokenAuthorizationFilter>();
}
}
As such, I then made my own version of ValidateAntiforgeryTokenAuthorizationFilter
public class ValidateAjaxAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
{
private readonly IAntiforgery _antiforgery;
private readonly ILogger _logger;
public ValidateAjaxAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery, ILoggerFactory loggerFactory)
{
if (antiforgery == null)
{
throw new ArgumentNullException(nameof(antiforgery));
}
_antiforgery = antiforgery;
_logger = loggerFactory.CreateLogger<ValidateAjaxAntiforgeryTokenAuthorizationFilter>();
}
public async Task OnAuthorizationAsync(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (IsClosestAntiforgeryPolicy(context.Filters) && ShouldValidate(context))
{
try
{
await _antiforgery.ValidateRequestAsync(context.HttpContext);
}
catch (AjaxAntiforgeryValidationException exception)
{
_logger.LogInformation(1, string.Concat("Ajax Antiforgery token validation failed. ", exception.Message));
context.Result = new BadRequestResult();
}
}
}
protected virtual bool ShouldValidate(AuthorizationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return true;
}
private bool IsClosestAntiforgeryPolicy(IList<IFilterMetadata> filters)
{
// Determine if this instance is the 'effective' antiforgery policy.
for (var i = filters.Count - 1; i >= 0; i--)
{
var filter = filters[i];
if (filter is IAntiforgeryPolicy)
{
return object.ReferenceEquals(this, filter);
}
}
Debug.Fail("The current instance should be in the list of filters.");
return false;
}
}
However, I cannot find the proper Nuget package and namespace that contains IAntiforgeryPolicy. While I found the interface on GitHub - what package do I find it in?
My next attempt was to instead go after the IAntiforgery injection, and replace the DefaultAntiforgery with my own AjaxAntiforgery.
public class AjaxAntiforgery : DefaultAntiforgery
{
private readonly AntiforgeryOptions _options;
private readonly IAntiforgeryTokenGenerator _tokenGenerator;
private readonly IAntiforgeryTokenSerializer _tokenSerializer;
private readonly IAntiforgeryTokenStore _tokenStore;
private readonly ILogger<AjaxAntiforgery> _logger;
public AjaxAntiforgery(
IOptions<AntiforgeryOptions> antiforgeryOptionsAccessor,
IAntiforgeryTokenGenerator tokenGenerator,
IAntiforgeryTokenSerializer tokenSerializer,
IAntiforgeryTokenStore tokenStore,
ILoggerFactory loggerFactory)
{
_options = antiforgeryOptionsAccessor.Value;
_tokenGenerator = tokenGenerator;
_tokenSerializer = tokenSerializer;
_tokenStore = tokenStore;
_logger = loggerFactory.CreateLogger<AjaxAntiforgery>();
}
}
I got this far before I stalled out because there is no generic method on ILoggerFactory for CreateLogger<T>(). The source code for DefaultAntiforgery has Microsoft.Extensions.Options, but I cannot find that namespace in any Nuget package. Microsoft.Extensions.OptionsModel exists, but that just brings in the IOptions<out TOptions> interface.
To follow all of this up, once I do get the Authorization Filter to work, or I get a new implementation of IAntiforgery, where or how do I register it with the dependency injection to use it - and only for the actions that I will be accepting Ajax requests?

I had similar issue. I don't know if any changes are coming regarding this in .NET but, at the time, I added the following lines to ConfigureServices method in Startup.cs, before the line services.AddMvc(), in order to validate the AntiForgeryToken sent via Ajax:
services.AddAntiforgery(options =>
{
options.CookieName = "yourChosenCookieName";
options.HeaderName = "RequestVerificationToken";
});
The AJAX call would be something like the following:
var token = $('input[type=hidden][name=__RequestVerificationToken]', document).val();
var request = $.ajax({
data: { 'yourField': 'yourValue' },
...
headers: { 'RequestVerificationToken': token }
});
Then, just use the native attribute [ValidadeAntiForgeryToken] in your Actions.

I've been wrestling with a similar situation, interfacing angular POSTs with MVC6, and came up with the following.
There are two problems that need to be addressed: getting the security token into MVC's antiforgery validation subsystem, and translating angular's JSON-formatted postback data into an MVC model.
I handle the first step via some custom middleware inserted in Startup.Configure(). The middleware class is pretty simple:
public static class UseAngularXSRFExtension
{
public const string XSRFFieldName = "X-XSRF-TOKEN";
public static IApplicationBuilder UseAngularXSRF( this IApplicationBuilder builder )
{
return builder.Use( next => context =>
{
switch( context.Request.Method.ToLower() )
{
case "post":
case "put":
case "delete":
if( context.Request.Headers.ContainsKey( XSRFFieldName ) )
{
var formFields = new Dictionary<string, StringValues>()
{
{ XSRFFieldName, context.Request.Headers[XSRFFieldName] }
};
// this assumes that any POST, PUT or DELETE having a header
// which includes XSRFFieldName is coming from angular, so
// overwriting context.Request.Form is okay (since it's not
// being parsed by MVC's internals anyway)
context.Request.Form = new FormCollection( formFields );
}
break;
}
return next( context );
} );
}
}
You insert this into the pipeline with the following line inside the Startup.Configure() method:
app.UseAngularXSRF();
I did this right before the call to app.UseMVC().
Note that this extension transfers the XSRF header on any POST, PUT or DELETE where it exists, and it does so by overwriting the existing form field collection. That fits my design pattern -- the only time the XSRF header will be in a request is if it's coming from some angular code I've written -- but it may not fit yours.
I also think you need to configure the antiforgery subsystem to use the correct name for the XSRF field name (I'm not sure what the default is). You can do this by inserting the following line into Startup.ConfigureServices():
services.ConfigureAntiforgery( options => options.FormFieldName = UseAngularXSRFExtension.XSRFFieldName );
I inserted this right before the line services.AddAntiforgery().
There are several ways of getting the XSRF token into the request stream. What I do is add the following to the view:
...top of view...
#inject Microsoft.AspNet.Antiforgery.IAntiforgery af
...rest of view...
...inside the angular function...
var postHeaders = {
'X-XSRF-TOKEN': '#(af.GetTokens(this.Context).FormToken)',
'Content-Type': 'application/json; charset=utf-8',
};
$http.post( '/Dataset/DeleteDataset', JSON.stringify({ 'siteID': siteID }),
{
headers: postHeaders,
})
...rest of view...
The second part -- translating the JSON data -- is handled by decorating the model class on your action method with [FromBody]:
// the [FromBody] attribute on the model -- and a class model, rather than a
// single integer model -- are necessary so that MVC can parse the JSON-formatted
// text POSTed by angular
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult DeleteDataset( [FromBody] DeleteSiteViewModel model )
{
}
[FromBody] only works on class instances. Even though in my case all I'm interested in is a single integer, I still had to dummy up a class, which only contains a single integer property.
Hope this helps.

Using a anti forgery token in a Ajax call is possible but if you are trying to secure a Api I really would suggest using a Access Token instead.
If you are relying on a identity token stored in a cookie as authentication for your Api, you will need to write code to compensate for when your cookie authentication times out, and your Ajax post is getting redirected to a login screen. This is especially important for SPAs and Angular apps.
Using a Access Token implementation instead, will allow you to refresh you access token (using a refresh token), to have long running sessions and also stop cookie thiefs from accessing your Apis.. and it will also stop XSRF :)
A access token purpose is to secure resources, like Web Apis.

Related

Handle SubDomain in Asp.net Core 3.1 application

I'm working on an asp.net core 3.1 application (MVC), and as a requirement, every account should have its subdomain (ex : mystore.domain.com) and its data. So I'm trying to figure out how to add the subdomain part in the routing pattern, and catch it in my controller in order to get the user data, and return it in a view.
I've done some research and found solutions for asp.net core version 2, unfortunettly, it does not work on version 3 (so much have changed) this article for example.
Summary :
User types : mystore.domain.com or mystore.domain.com\store
I catch the subsomain "mystore", search the database for the user data, and render a view.
You could use a filter, specifically, an action filter, which could:
Run code immediately before and after an action method is called.
Can change the arguments passed into an action.
Can change the result returned from the action.
Are not supported in Razor Pages.
An example is
public class MySampleActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// Do something before the action executes.
MyDebug.Write(MethodBase.GetCurrentMethod(), context.HttpContext.Request.Path);
}
public void OnActionExecuted(ActionExecutedContext context)
{
// Do something after the action executes.
MyDebug.Write(MethodBase.GetCurrentMethod(), context.HttpContext.Request.Path);
}
}
Here you could prepare a scoped service, load the user based on the service and then reuse it in any service that requires that data.
Even without the filter, you could simply create a UserService with a scoped lifetime, load the user there and use it anywhere in your services.
In our system we are doing something similar:
A service to load the session data:
public class ClientTokenService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ClientTokenService(
IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Profile LoadProfile()
{
if (_httpContextAccessor.HttpContext.User == null)
{
throw new Exception("No user claims found to load Profile");
}
var user = _httpContextAccessor.HttpContext.User;
var numberType = (NumberType)int.Parse(user.FindFirst("numberType").Value);
var profileType = (PackagePlan)int.Parse(user.FindFirst("profileType").Value);
var lineOfBusiness = (LineOfBusiness)int.Parse(user.FindFirst("lineOfBusiness").Value);
// More stuff
// Prepare the profile data
return new Profile(
user.FindFirst("number").Value,
user.FindFirst("contractId").Value,
numberType,
profileType,
user.FindFirst("cc")?.Value,
user.FindFirst("app").Value,
user.FindFirst("clickId")?.Value,
user.FindFirst("wifi") != null,
lineOfBusiness
);
}
}
This service can be transient, and then a scoped service which saves the data
public class ClientSessionContext
{
public Profile Profile { get; }
public ClientSessionContext(
ClientTokenService sessionService)
{
Profile = sessionService.LoadProfile();
}
}
Declare this service as scoped, so this class is initialized just once per request
Statup.cs
services.AddScoped<ClientSessionContext>();
Then just inject this service anywhere where you need access to the user data.

.NET Core: access request data on bad request when ApiController shortcuts the pipeline

For all those stumbling accross logging issues I created a small public playground on Github. Feel free to join.
I am still experimenting with the one hits all logging filter for my requests.
The goal is simple. Instead of writing the logs in each controller method implement them once in a filter (or middleware) and apply them globally to all. This is used when something wents wrong I can see which data was used causing the problem.
so for example a controller action (with the globally applied filter)
[HttpPut("{id:int}")
public IActionResult UpdateModel(int id, [FromBody] MyRequestModel request) {}
would create logs like
[Timestamp] INFO: MyNamespace.Controllers.MyModelController.UpdateModel
{
"Parameters": {
"Id": 1,
"Request": {
"Name": "abc",
"SomeInt": 3
}
}
}
Pretty, no more logging manually in each method.
But wait: it's an API and I used the [ApiController] attribute and the request model has invalid data (let's say Name = "a" but it needs to be at least of length of 3).
This gives me 3 problems
ActionFilter.OnActionExecuting is shortcutted and does not get called (due to the ApiController)
the binding of the arguments seems to be skipped and the bound (invalid) data is not applied to the ActionArguments
only the ResultFilter.OnResultExecuted is called but there seems to be no way for accessing/logging the invalid data
This somehow means logging works only when everything goes well but aren't the most interesting logs those were things go wrong?
public void OnActionExecuting(ActionExecutingContext context)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogInformation
(
JsonConvert.SerializeObject
(
new { Parameters = context.ActionArguments },
Formatting.Indented
)
);
}
Point 1: I of course could remove the ApiController from each controller and go back to return BadRequest results manually. But I like the centralized approach and would like to stick with it.
I liked the model binding approach giving me classes to serialize for the logs (instead of reading the request body manually as one string). With a custom Json contract resolver I am able to mark model properties as sensitive and they are hidden in the logs (for those how care about security).
So my actual question:
Is there a way to get the model binding values in a ResultFilter or are they thrown away totally?
Or is there a way to hook into the model binding/model validation and write the logs there before they get thrown away?
Both especially for the case where the ApiController attribute starts shortcutting the filter pipeline.
public class LogFilter : IActionFilter, IResultFilter, IExceptionFilter
{
private readonly ILoggerFactory _loggerFactory;
public LogFilter(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
}
public void OnActionExecuting(ActionExecutingContext context)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogInformation
(
JsonConvert.SerializeObject
(
new
{
RequestBody = ReadBodyAsString(context.HttpContext.Request),
Parameter = context.ActionArguments
},
Formatting.Indented
)
);
}
public void OnActionExecuted(ActionExecutedContext context) {}
public void OnResultExecuting(ResultExecutingContext context)
{
if (!context.ModelState.IsValid)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogWarning
(
JsonConvert.SerializeObject
(
new
{
RequestBody = ReadBodyAsString(context.HttpContext.Request),
ModelErrors = context.ModelState
.Where(kvp => kvp.Value.Errors.Count > 0)
.ToDictionary
(
kvp => kvp.Key,
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
)
},
Formatting.Indented
)
);
}
}
public void OnResultExecuted(ResultExecutedContext context) {}
public void OnException(ExceptionContext context)
{
_loggerFactory
.CreateLogger(context.ActionDescriptor.DisplayName.Split(" ")[0])
.LogError
(
context.Exception,
JsonConvert.SerializeObject
(
new
{
//RequestBody = ReadBodyAsString(context.HttpContext.Request) // body is disposed here
},
Formatting.Indented
)
);
}
}
You can create a class which implements OnActionExecuting method of IActionFilter. And register it in global filter so that it applies to all controllers and actions. When exception happens for model binding(coz of length), your OnActionExecuting method still gets called even when [ApiController] is used and you can log it there.
Eg.
public class MyActionFilterAttribute: IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
//Your logic to log the request, you can get the details from
//context parameter
//You can check if model state is valid also by using the property
//context.ModelState.IsValid
}
}
In Startup.cs, you need to set SuppressModelStateInvalidFilter to true. This will not return the 400 status automatically. Your controller method still gets called and since you have action filter, the OnActionExecuting gets called.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(
config =>
{
config.Filters.Add(new MyActionFilterAttribute());
})
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
}
I think you are right about the ActionFilter only catch successful requests.
For your invalid requests, maybe you can have a look at the InvalidModelStateResponseFactory.
When using options.InvalidModelStateResponseFactory you can read your Model and validationerrors.

Dynamic Routing in BaseUrl in Asp.Net Core OData 4.0

I am currently developing an OData Api for a C# Asp.Net Core application.
To stay in specifications of our API the URL needs to follow our multi-tenant architecture:
https://website.com/api/tenants/{tenantId}/odata/
Since OData 4.0 has no specification how to implement a dynamic base url I implemented the following workaround: Use a middleware to replace the dynamic tenantId in the HTTP Context with a the static string "tenantId". Now I need to find a way to modify/manipulate the OData metadata to reverse back this workaround in the response.
Implementation Example
Starup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
private IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDependencies(Configuration);
services.AddDbContext<DBContext>();
services.AddOData();
services.AddODataQueryFilter();
services.AddAutoMapper();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Custom Workaround Middleware
app.Use(async (context, next) =>
{
// TGis Method parses the tenant id from the Request.Path, replaces it and wries it to the context.Items to maintain the information for later
(Microsoft.AspNetCore.Http.HttpContext contextwTid, System.Guid tenantGuid) = ODataHelper.ParseTenantIDToContext(context);
context = contextwTid;
await next.Invoke();
});
app.UseMvc(b =>
{
b.Select().Filter().OrderBy().MaxTop(100).Count();
b.MapODataServiceRoute(
routeName: "odata",
routePrefix: "api/tenants/tenantId/odata",
model: ODataHelper.GetEdmModel());
});
}
ODataHelper:
...
public static (Microsoft.AspNetCore.Http.HttpContext, Guid) ParseTenantIDToContext(Microsoft.AspNetCore.Http.HttpContext context)
{
System.Guid tenantGuid = System.Guid.Empty;
if (context.Request.Path.ToString().Split('/').Length > 3 && context.Request.Path.ToString().ToLower().Contains("odata"))
{
bool isValidGUID = System.Guid.TryParse(context.Request.Path.ToString().Split('/')[3], result: out tenantGuid);
if (isValidGUID)
context.Request.Path = context.Request.Path.Value.Replace(context.Request.Path.ToString().Split('/')[3], "tenantId");
context.Items["tenantId"] = tenantGuid.ToString();
}
return (context, tenantGuid);
}
...
Example controller:
public class ClientsController : ODataController
{
private readonly DBService<Client> _service;
public ClientsController(DBService<Client> service)
{
_service = service;
}
[HttpGet]
[EnableQuery]
[ODataRoute("Clients")]
public async Task<IEnumerable<Client>> Get(
ODataQueryOptions<Client> options)
{
System.Guid tenantId = ODataHelper.GetTenantIDFromContext(this.HttpContext);
IQueryable res = await _service.Get(
tenantId,
AuthorizationHelper.GetSubjectId(tenantId, User),
AuthorizationHelper.GetAllowedUserRoles(RoleType.Reporting),
options,
null);
return new List<Client>(res.Cast<Client>());
}
}
Questions:
Is there a better way to implement dynamic base routing in OData with Asp.Net Core?
Is there any way to manipulate the request or the OData Metadata. In detail the response needs to display the original url with the dynamic tenantId in "#OData.context" and (for the future) in the OData paging metadata.
Research/Googling so far:
ODataMediaTypeFormatter in WebApi but I found no implementation for .net Core.
Solution for WebApi 2.2 but there is no UrlHelper in .net Core.
Offical OData WebApi tutorial but this does not prove a simpler way than the workaorund.
EDIT 2:
Sometimes you think so complicated that you miss the obvious. Solution for dynamic routing with OData:
Startup.cs
app.UseMvc(b =>
{
b.Select().Filter().OrderBy().MaxTop(100).Count();
b.MapODataServiceRoute(
routeName: "odata",
routePrefix: "api/tenants/{tenantId}/odata",
model: ODataHelper.GetEdmModel());
});
Controller:
[HttpGet]
[EnableQuery]
[ODataRoute("Clients")]
public async Task<IEnumerable<Client>> Get(
ODataQueryOptions<Client> options,
[FromRoute] Guid tenantId)
{
IQueryable res = await _service.Get(
tenantId,
AuthorizationHelper.GetSubjectId(tenantId, User),
AuthorizationHelper.GetAllowedUserRoles(RoleType.Reporting),
options,
null);
return new List<Client>(res.Cast<Client>());
}
I leave my workaround in here for the case that somebody could use it:
After a considerable research within in OData .Net Core Implementation I finally found, that my first provided link "ODataMediaTypeFormatter in WebApi" already provided a solution to my workaround.
Firstly, the BaseAddressFactory can only the given HTTP request. Therfore, I needed to change the following Code:
public static (Microsoft.AspNetCore.Http.HttpContext, Guid) ParseTenantIDToContext(Microsoft.AspNetCore.Http.HttpContext context)
{
System.Guid tenantGuid = System.Guid.Empty;
if (context.Request.Path.ToString().Split('/').Length > 3 && context.Request.Path.ToString().ToLower().Contains("odata"))
{
bool isValidGUID = System.Guid.TryParse(context.Request.Path.ToString().Split('/')[3], result: out tenantGuid);
if (isValidGUID)
{
context.Request.Path = context.Request.Path.Value.Replace(context.Request.Path.ToString().Split('/')[3], "tenantId");
context.Items["tenantId"] = tenantGuid.ToString();
context.Request.Headers.Remove("tenantId");
context.Request.Headers.Append("tenantId", tenantGuid.ToString());
}
}
return (context, tenantGuid);
}
In this section, I save the required tenantId not only in the HTTPContext but also as a special header within the HTTPRequest.
The main solution is to provide a special BaseAddressFactory function which manipulates the base address OData uses to build the metadata. As implemetation I add the following code within the ConfigureServices after adding OData via services.AddOData():
services.AddMvc(op =>
{
foreach (var formatter in op.OutputFormatters
.OfType<ODataOutputFormatter>())
{
formatter.BaseAddressFactory = ODataHelper.CustomBaseAddressFactory;
}
foreach (var formatter in op.InputFormatters
.OfType<ODataInputFormatter>())
{
formatter.BaseAddressFactory = ODataHelper.CustomBaseAddressFactory;
}
});
My ODataHelper.CustomBaseAddressFactory looks like this:
public static Uri CustomBaseAddressFactory (HttpRequest request)
{
Guid tenantGuid = GetTenantIDFromRequest(request);
request.Headers.Remove("tenantId");
Uri std = ODataInputFormatter.GetDefaultBaseAddress(request);
string ret = replaceTentantIdInURL(std.ToString(), tenantGuid);
return ret[ret.Length - 1] != '/' ? new Uri(ret + '/') : new Uri(ret);
}
To provide as much compability as possible I use the standard ODataInputFormatter.GetDefaultBaseAddress and afterwards replaces my static placeholder again.
EDIT
This way of saving the tenantId is quite insecure since the request headers can be created by the enduser as well. In the end, I decided to receive the ID out of our Authorization claims which provides it. Therefore, the user can not attack this workaround.

ASP.Net 5 AuthorizationHandler Fail Redirect

I am trying to add a custom authorization policy that can check a delimited list of groups supplied in a json config file. I am using ASP.Net 5 - MVC 6, along with windows authentication.
Everything is working fine, except for when I call Fail. Then nothing happens. A Blank screen is shown. Here is my HandleRequirementAsync method. I have tried various values for the task result. I have been googling like a madman, but with no luck. Hopefully someone can help.
DESIRED RESULT: I would like to redirect to a custom page on failure, but if that is not possible, at least be able to redirect back to the login page. The only thing that seems to have any effect is to throw an exception.
The pertinent registration code in Startup:
var appSettings = Configuration.GetSection("AppSettings");
services.Configure<Models.AppSettings>(appSettings);
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("RoleAuth", policy => policy.Requirements.Add(new RolesRequirement(appSettings["AllowedGroups"])));
});
services.AddSingleton<IAuthorizationHandler, RoleAuthorizationHandler>();
And the authorization classes:
public class RolesRequirement : IAuthorizationRequirement
{
public RolesRequirement(string groups)
{
Groups = groups;
}
public string Groups { get; private set; }
}
public class RoleAuthorizationHandler : AuthorizationHandler<RolesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesRequirement requirement)
{
if (!string.IsNullOrWhiteSpace(requirement.Groups))
{
Console.WriteLine(requirement.Groups);
var groups = requirement.Groups.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
//we could check for group membership here.... maybe???
foreach (var group in groups)
{
if (context.User.IsInRole(group))
{
context.Succeed(requirement);
return Task.FromResult(0);
}
}
}
else
{
context.Succeed(requirement);
}
context.Fail();
return Task.FromResult(0);
}
}
The only way i have found of doing this is, don't use context.Fail(), instead do this:
replace:
context.Fail();
with:
var mvcContext = context.Resource as AuthorizationFilterContext;
mvcContext.Result = new RedirectToActionResult("Action", "Controller", null);
context.Succeed(requirement);
allowing the context to succeed, will execute the context, which is now a redirect.
I went with what herostwist suggested, but Policies can challenge or forbid. After intesive research I came across what gives you direct access to the AuthorizationFilterContext like this (because they follow the naming convention and inherit from AuthorizeAttribute:
public class BudgetAccessFilterAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
//context.HttpContext.User.Identity.Name
//TODO: determine if user has access to budget controllers, all of them could inherit from a Common Controller with this Filter
if (false)
{
//if no access then
context.Result = new RedirectToActionResult("Index", "Home", null);
}
}
}
You can then decorate your controllers like this:
[BudgetAccessFilter]
public class BudgetItemController : Controller
{
}
And if you will have lots of controllers with the same checking, then they can all inherit from a base class with the annotation like this:
[BudgetAccessFilter]
public class BCommonController : Controller
{
}
And then clean controllers:
public class BudgetItemController : BCommonController
{
}
After trying Herotwists answer and seeing that it no longer works in .NET Core (context.Resource as AuthorizationFilterContext always returns NULL), I came up with this which seems to work fine in .NET 5. It's a bit hacky though .... I'd really like to see how this should be done. Surely it should be possible?
Anyway, here goes:
if (accessAllowed)
{
context.Succeed(requirement);
}
else
{
var mvcContext = (context.Resource as Microsoft.AspNetCore.Http.DefaultHttpContext);
if (mvcContext != null)
{
mvcContext.Response.Redirect("/the-url-you-want-to-redirect-to");
}
}
I'm using cookie authentication instead of windows but in my Configure method in the Startup.cs I have the following piece of code which tells it where to go
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = "/account/login",
AuthenticationScheme = "Cookies",
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
I don't know where you can configure the redirect result, but at least I was able to create such a "Account/AccessDenied.cshtml" file which will be shown in the fail case. "Account" is my class name and when fail happened, browser was redirected to this Url: (http://localhost:39339/Account/AccessDenied?ReturnUrl=%2Fapp%2Fequipments)
Here is my controller code (Web/AccountController.cs) as well.
public class AccountController : Controller
{
public IActionResult AccessDenied()
{
return View();
}
}

NULL HttpContext for batched OData requests in Web API - wrong design pattern?

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?

Categories