Search code examples
angularasp.net-coreauthenticationasp.net-web-apicsrf

How to refresh CSRF token on login when using cookie authentication without identity in ASP .NET Core Web API


I have an ASP .NET Core 3.1 backend, with angular 9 frontend (based on dotnet angular template, just with updated angular to v9). I use cookie authentication (I know JWT is more suited for SPAs, take this as an experiment) and I also added support for CSRF protection on server side:

services.AddAntiforgery(options =>
{
   options.HeaderName = "X-XSRF-TOKEN"; // angular csrf header name
});

I have server side setup to automatically check CSRF using

options => options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())

so GET requests are not checked against CSRF, but POST are.

At the very beginning, the angular app makes a GET request to api/init to get some initial data before bootstrapping. On server-side this action initializes CSRF as follows:

// init action body
var tokens = _antiForgery.GetAndStoreTokens(HttpContext);
Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions
{
   HttpOnly = false
});
// return some inital data DTO

This works as expected - the GET response contains 2 CSRF cookies - first being ASP .NET core default CSRF cookie .AspNetCore.Antiforgery... and second being XSRF-TOKEN that angular will read and put into X-XSRF-TOKEN header for subsequent requests.

If afterwards I do login (POST request containing credentials to api/auth/login) from the angular app, everything works - request is POSTed including X-XSRF-TOKEN header and CSRF validation passes, so if credentials are correct the user is logged in.

Now here is where the problems begin. The ASP .NET server app uses cookie authentication without identity as described here https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1. In login action also CSRF token needs to be regenerated as with authentication the CSRF token starts including authenticated user identity. Therefore my login action looks like this:

public async Task<IActionResult> Login(CredentialsDto credentials)
{
   // fake user credentials check
   if (credentials.Login != "admin" || credentials.Password != "admin")
   {
      return Unauthorized();
   }

   var claimsIdentity = new ClaimsIdentity(new[]
   {
     new Claim(ClaimTypes.Name, "theAdmin"),
   }, CookieAuthenticationDefaults.AuthenticationScheme);

   var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
   await HttpContext.SignInAsync(claimsPrincipal); 

   // refresh antiforgery token on login (same code as in init action before)
   var tokens = _antiForgery.GetAndStoreTokens(HttpContext);
   Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions 
   {
       HttpOnly = false
   });

   return new JsonResult(new UserDto { Id = 1, Login = "theAdmin" });
}

This however does not work. The response contains the XSRF-TOKEN cookie, but subsequent POST request (in my case its logout = POST to api/auth/logout) fails with 400, despite angular correctly putting this cookie value into X-XSRF-TOKEN header. I believe the reason is that the dafault .AspNetCore.Antiforgery... cookie is not being set in the response for some reason, therefore retains the original value even after login and thus CSRF check fails as the values don't match,

How does one properly refresh the CSRF token is such scenario?


Solution

  • After doing some more trial and error and also searching ASP .NET github and finding https://github.com/dotnet/aspnetcore/issues/2783 and https://github.com/aspnet/Antiforgery/issues/155, it seems that what I want to achieve is not doable within a single request, because the user's identity does not change during single request processing (apparently by design).

    The only way to make it work (aka. refresh CSRF token after login/logout) is to make additional request afterwards, where CSRF token will be refreshed properly based on the newly acquired identity.

    I achieved this by my login action returning a 302 redirect, which when followed executes CSRF refresh. This also needs to be done for logout.

    Side effect being that the amount of requests doubled and there might be issues with angular following redirects (worked for me in Chrome using Angular 9, but there are a lot of complains on the internet about angular's HttpClient not following redirects). Also you might end up in a limbo state when login request succeeds, but following the redirect to token refresh fails for some reason - then you are stuck with outdated CSRF token. So not great, not terrible...