Customizing Antiforgery Behavior For Cookies And Bearer Tokens

4/15/2022

By: Shaun Walker

Cross-site request forgery (also known as XSRF or CSRF) is an attack against web-hosted apps whereby a malicious web app can influence the interaction between a client browser and a web app that trusts that browser. These attacks are possible because web browsers send some types of authentication tokens automatically with every request to a website. This form of exploit is also known as a one-click attack or session riding because the attack takes advantage of the user's previously authenticated session.

When a user authenticates using their username and password, they're issued a token, containing an authentication ticket that can be used for authentication and authorization. The token is stored as a cookie that's sent with every request the client makes. CSRF attacks are possible against web apps that use cookies for authentication (and using HTTPS doesn't prevent a CSRF attack).

The most common approach to defending against CSRF attacks is to use the Synchronizer Token Pattern (STP). When using STP the server sends a token associated with the current user's identity to the client, the client sends back the token to the server for verification, and if the server receives a token that doesn't match the authenticated user's identity, the request is rejected. In order for this to work, the token must be unique and unpredictable. .NET Core's Antiforgery middleware provides this capability.

The most common way this is implemented is by using the [ValidateAntiForgeryToken] action filter on your actions or controllers (this is enabled by default on Razor Pages). However if your application supports multiple types of authorization it has some challenges. This dual authorization approach is common with APIs where you have a same-domain web application that relies on cookies but also supports external clients connecting via bearer tokens. In this scenario the [ValidateAntiForgeryToken] action filter will behave correctly for the cookie scenarios but will reject all of your bearer token requests because they do not contain a valid antiforgery token (as this is not something you would expect an external client to provide). Luckily, there is an elegant solution.

Based on the default antiforgery token filter, you can create your own custom filter which contains an extra condition to ignore antiforgery validation if a bearer token was provided with the request. The key is the ShouldValidate method.

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

namespace Oqtane.Security
{
    public class AutoValidateAntiforgeryTokenFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
    {
        private readonly IAntiforgery _antiforgery;

        public AutoValidateAntiforgeryTokenFilter(IAntiforgery antiforgery)
        {
            _antiforgery = antiforgery;
        }

        public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (!context.IsEffectivePolicy(this))
            {
                return;
            }

            if (ShouldValidate(context))
            {
                try
                {
                    await _antiforgery.ValidateRequestAsync(context.HttpContext);
                }
                catch
                {
                    context.Result = new AntiforgeryValidationFailedResult();
                }
            }
        }

        protected virtual bool ShouldValidate(AuthorizationFilterContext context)
        {
            // ignore antiforgery validation if a bearer token was provided
            if (context.HttpContext.Request.Headers.ContainsKey("Authorization"))
            {
                return false;
            }

            // ignore antiforgery validation for GET, HEAD, TRACE, OPTIONS
            var method = context.HttpContext.Request.Method;
            if (HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsTrace(method) || HttpMethods.IsOptions(method))
            {
                return false;
            }

            // everything else requires antiforgery validation (ie. POST, PUT, DELETE)
            return true;
        }
    }
}

The filter can be encapsulated into an attribute:

using System;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;

namespace Oqtane.Security
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class AutoValidateAntiforgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter
    {
        public int Order { get; set; } = 1000;

        public bool IsReusable => true;

        public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
        {
            return serviceProvider.GetRequiredService();
        }
    }
}

And in the Startup class of your application, you can apply the filter at a global level when adding MVC:

    services.AddMvc(options =>
    {
        options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
    })

(the AutoValidateAntiforgeryTokenFilter will need to be registered as a Singleton service as well)

With this custom filter in place, antiforgery will be enforced for your cookie-based requests and will be ignored for bearer token requests. You can obviously customize the ShouldValidate method of the AutoValidateAntiforgeryTokenFilter even further if you have additional logic or behavior which is specific to your application.


Do You Want To Be Notified When Blogs Are Published?
RSS