Csrf cookies in Dotnet with axios

CSRF validation in .Net Core is a breeze when using Razor syntax or even jQuery, but how should you approach this for a front-end project that uses axios? The official documentation gets you almost all of the way, but axios has a great feature to make this as painless as possible with the axios.defaults.xsrfHeaderName and axios.defaults.xsrfCookieName with minimal changes in .Net Core land.

Overview:

  1. Scaffold new MVC project
  2. Follow official docs + extend
  3. Add an Antiforgery enabled endpoint to test
  4. Add axios
  5. make axios call the endpoint with AntiforgeryValidation

Scaffold new Mvc project

first scaffold a new MVC project in dotnet core:

dotnet new mvc --name Csrf.Sample 

Follow official docs + extend

First configure Starup.cs while referencing the Starup.ConfigureServices docs and the extra annotations in comments below:

// Startup.cs

// ...
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
        services.AddAntiforgery(options => {    
            options.HeaderName = "X-XSRF-TOKEN";
        });
        
        services.Configure<CookiePolicyOptions>(options =>
        {
            // All unessential cookies (including anti-csrf) will not be allowed until consent is granted by default.
            options.CheckConsentNeeded = context => true;
        });
        // ...
    }

    public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
    {
        // ...
        app.Use(next => context =>
        {
            string path = context.Request.Path.Value;

            if (
                // You can modify what gets a csrf token here
                string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
            {
                var tokens = antiforgery.GetAndStoreTokens(context);

                // IF using CookiePolicyOptions IsEssential needs to be true
                context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, 
                    new CookieOptions() { HttpOnly = false, IsEssential = true }); 
            }

            return next(context);
        });

        // ...
    }

Some important points:

  • if options.HeaderName is null, it will only work for form data (ref).
  • options.Cookie.Name must not match the context.Response.Cookies.Append token name.
  • if using CookiePolicyOptions, the context.Response.Cookies.Append CookieOptions needs IsEssential to add to the client before consent.

Start the application (dotnet run), go to the homepage (https://localhost:5001) and open up the developer tools. Confirm that the client code can read the XSRf-COOKIE in the network tab as well as document.cookie in the console section like the picture below:

confimring the cookie is accessible from the client

Add an Antiforgery enabled endpoint to test

In Controllers/HomeController.cs add the following route:

[ValidateAntiforgeryToken]
public IActionResult WelcomeMessage(){
    return Ok("Great!");
}

Next, to test this with axios.

Add Axios

First the Axios libary is needed. For brevity and ease-of-use, use the CDN version In Views/Shared/_Layout.cshtml add the following parts below the annotation <!-- Added here:

// _Layout.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
    <!-- ... omitted for brevity -->
</head>
<body>
    <!-- ... omitted for brevity -->
<footer class="border-top footer text-muted">
    <div class="container">
        &copy; 2019 - Csrf.Sample - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
    </div>
</footer>

<environment include="Development">
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>

    <!-- Added here for development -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</environment>
<environment exclude="Development">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
            asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
            asp-fallback-test="window.jQuery"
            crossorigin="anonymous"
            integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
    </script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js"
            asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
            asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
            crossorigin="anonymous"
            integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRTLCcmivcbNJWm2rs5C8PRhcEn3czEjhAO9o">

    </script>
    <!-- added here for production -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>

@RenderSection("Scripts", required: false)
</body>
</html>

After the axios library is added, axios can now call the endpoint from Home.cshtml. In Views/Home/Index.cshtml add the following snippet to the end:

@section scripts {
    <script>
        document.addEventListener("DOMContentLoaded", function() {
            window.axios.get('/home/WelcomeMessage').then((response) => {
                alert(response.data);
            }).catch((err) => {
                console.log(err);
            })
        });
    </script>
}

Where’s the magic?

Axios has a default setting for CSRF cookies halfway down the Request Config section that will explains it.

// `xsrfCookieName` is the name of the cookie to use as a value for xsrf token
xsrfCookieName: 'XSRF-TOKEN', // default

// `xsrfHeaderName` is the name of the http header that carries the xsrf token value
xsrfHeaderName: 'X-XSRF-TOKEN', // default

The defaults in axios are what we set in the Startup.cs. Whenever axios makes a request, it will check the document.cookie for a value called XSRF-TOKEN and add it as a header X-XSRF-TOKEN.

Some considerations:

This will not provide a new token for every request. If there is interest, I may look into it. Additionally, this integration works just as well for Vue/Nuxt/React apps that use axios as it is the default behavior.