Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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://docs.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?

like image 893
yohny Avatar asked May 24 '20 00:05

yohny


People also ask

How do I get my CSRF token in login?

Your csrf token is a token that represent you, but not other. So you have a way to exchange your credentials to get that token. That is the first request to the server with your credentals ( username/password) , here is login form. The only wait to get csrf token is your username and password.

Can we put CSRF token in cookie?

The cookie contains the csrf token, as sent by the server. The legitimate client must read the csrf token out of the cookie, and then pass it in the request somewhere, such as a header or in the payload.

How do I get CSRF token from response?

To fetch a CRSF token, the app must send a request header called X-CSRF-Token with the value fetch in this call. The server generates a token, stores it in the user's session table, and sends the value in the X-CSRF-Token HTTP response header.


1 Answers

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...

like image 101
yohny Avatar answered Nov 15 '22 08:11

yohny