Implementation of CSRF token in Stateless ASP.NET Core 8 Web API without Views/MVC!

The need to write this article arose because I did not find any other good tutorial. Every search result I saw was with serverside rendered views. So I scoured the web countless hours and learned every bit of CSRF tokens and cookies and everything related to it. Finally, I got a pretty good sense of everything surrounding the topic.

My environments were backend in .NET and frontend in client-side rendered project.
Here is my step-by-step guide on how I made it.

First I made changes to Program.cs file:

using Microsoft.AspNetCore.Antiforgery;

builder.Services.AddAntiforgery(options => 
{
    options.Cookie = new CookieBuilder {
         HttpOnly = true, // for security purposes, this means that when csrf endpoint responses with "set-cookie" header then the cookie is httpOnly which means the cookie is stored to the browser, BUT webpage's javascript can't access it.
         Domain = "your-site-url-here.com", 
         Expiration = new TimeSpan(TimeSpan.TicksPerMinute),
         Path = "/", // I had to put it to just a slash, otherwise the set-cookie would work for me. Maybe some configuration can fix other paths. 
         Name = "csrf"
         // SameSite = SameSiteMode.None, // for localhost I had to use SameSite.None, but in test server it broke things for me.
         // SecurePolicy = CookieSecurePolicy.Always // for localhost I had to use it according to Chrome's dev tool network tab for the set-cookie to apply to cookies. But did not need it in test server.
    },
    options.HeaderName = "X-CSRF-TOKEN"; // This name is also important, because the same name must be present in your request's headers from frontend
    options.SuppressXFrameOptionsHeader = false; // For Clickjacking
});

builder.Services.AddControllersWithViews(options => { // Only AddControllersWithViews works, "AddControllers" is not enough here. 
    options.Filters.Add(new Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute()); // Adds CSRF token validation to all controllers and their actions
});

// after var app = builder.Build();

app.UseAntiforgery();

Then I added a controller for getting CSRF token:

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Your.Controllers;

[ApiController]
[Route("api/csrf")]
public class CsrfController(IAntiforgery antiforgery) {
    [HttpGet("")]
    [AllowAnonymous]
    [IgnoreAntiforgeryToken] // So the CSRF token validation is turned off for this action because you have to get an acces to this endpoint always for a token.
    public IActionResult GetCsrf() {
        var tokenSet = antiforgery.GetAndStoreTokens(HttpContext); // This generates the tokens for your cookies and requests and adds needed response headers for your browser.
        return Ok(tokenSet.RequestToken); // The response contains the request token as in "Ok" method and if you look in browser response headers then there is "set-cookie" named header which contains cookie token which is automatically set to your browsers cookies which are inaccessible for javascript but the csrf cookie is sent automatically with the next request you make from frontend.
    }
}

Finally, I applied the CSRF token acquiring to all my API requests beforehand from the front-end:

const getCsrfToken = async () => {
    return fetch(`${APIURL}/api/csrf`)
        .then(data => data.json())
        .catch(error => console.error("Error fetchin CSRF token: ", error))
}

const apiFetch = async (url, params..., and so on) => {
     const csrfResult = await getCsrfToken();

    // in your fetch add "X-CSRF-TOKEN" to headers with value <csrfResult> (or request_token, depending on your casing settings in backend)
}

I hope this helps someone or if anyone sees some mistakes then please feel free to comment. Thank you for reading and until next time.

Leave a Comment