ValidateAntiForgeryToken in Ajax request with AspNet Core MVC

只愿长相守 提交于 2020-08-01 10:24:05

问题


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?


回答1:


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.




回答2:


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.




回答3:


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.



来源:https://stackoverflow.com/questions/36628666/validateantiforgerytoken-in-ajax-request-with-aspnet-core-mvc

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!