0

I have a web application that uses asp.net core (3.1) backend and angular front end (8.2.11). It uses asp.net Identity framework for user authentication. It store authentication token in local storage to be used as authentication header in requests. Everything is working in the sense controller endpoints are only accessible when a user is logged in, if logged out, typing an endpoint directly into browser would be rejected.

I am still not certain if such a setup prevent the Cross-Site Request Forgery (XSRF/CSRF) attacks. I know using cookie to store authentication token is susceptible to CSRF and I tried a little bit with the [ValidateAntiForgeryToken] attribute on some endpoint, it broke those endpoints of course. I know in Razor page, a form is automatically injected with anti-forgery token. So, do I need to set it up in my angular front-end? and if yes, how? (I've searched a bit on the web and the instructions are all over the place, quite messy with no clear consensus).

For Comment
  • 1,139
  • 4
  • 13
  • 25

2 Answers2

2

Step 1

Add a middleware to your middleware pipeline that generates an AntiforgeryToken, and embeds the token in a non-http-only cookie that's attached to the response:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddAntiforgery(options => {
            options.HeaderName = "X-XSRF-TOKEN";
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
    {
        ...;
        app.Use((context, next) => {
            var tokens = antiforgery.GetAndStoreTokens(httpContext);
            httpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { Path = "/", HttpOnly = false });
        });
    }
}

I created a little package for this that contains this middleware.

Step 2

Configure your angular app to read the value of the non-http-only cookie (XSRF-TOKEN) through javascript, and pass this value as a X-XSRF-TOKEN header for requests sent by the HttpClient:

@NgModule({
  declarations: [...],
  imports: [
    HttpClientModule,
    HttpClientXsrfModule.withOptions({
      cookieName: 'XSRF-TOKEN',
      headerName: 'X-XSRF-TOKEN'
    }),
    ...
  ],
  providers: [...],
  bootstrap: [AppComponent]
})
export class AppModule { }

Step 3

Now you can decorate your controller methods with the [ValidateAntiforgeryToken] attribute:

[ApiController]
[Route("web/v1/[controller]")]
public class PersonController : Controller
{
    private IPersonService personService;
    public PersonController(IPersonService personService)
    {
        this.personService = personService;
    }
    
    [HttpPost]
    [Authorize]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult<Person>> Post([FromBody] Person person)
    {
        var new_person = await personService.InsertPerson(person);
        return Ok(new_person);
    }
}

Step 4

Make sure the requests you're sending have the following type of url as stated here:

  • /my/url
  • //example.com/my/url

Wrong url:

  • https://example.com/my/url

Note

I use Identity Cookie authentication:

services.AddAuthentication(/* No default authentication scheme here*/)

Since the ASP.NET Core Authentication middleware only looks after the XSRF-TOKEN header, and not the X-XSRF-TOKEN cookie, you're no longer susceptible to Cross-site request forgery.

Spoiler

You will notice that right after signing in/out, the first webrequest that's being sent will still be blocked by XSRF protection. This is because the Identity does not change during the lifetime of the webrequest. So when sending the Login webrequest, the response will attach a cookie with a csrf-token. But this token is still generated with the identity from when you weren't signed in yet.

The same counts for sending the Logout webrequest, the response will contain a cookie with a csrf-token as if you're still signed in.

To solve this, you have to simply send another webrequest that does literally nothing, every time you've signed in/out. During this request you'll once again have the correct Identity in order to generate the csrf-token.

logoutClicked() {
  this.accountService.logout().then(() => {
    this.accountService.csrfRefresh().then(() => {
      this.activeUser = null;
    });
  }).catch((error) => {
    console.error('Could not logout', error);
  });
}

Same for login

this.accountService.login(this.email, this.password).then((loginResult) => {
  this.accountService.csrfRefresh().then(() => {
    switch (loginResult.status) {
      case LoginStatus.success:
        this.router.navigateByUrl(this.returnUrl);
        this.loginComplete.next(loginResult.user);
        break;
      default:
        this.loginResult = loginResult;
        break;
    }
  });
});

The contents of the csrfRefresh method

public csrfRefresh() {
  return this.httpClient.post(`${this.baseUrl}/web/Account/csrf-refresh`, {}).toPromise();
}

Server-side

[HttpPost("csrf-refresh")]
public async Task<ActionResult> RefreshCsrfToken()
{
    // Just an empty method that returns a new cookie with a new CSRF token.
    // Call this method when the user has signed in/out.
    await Task.Delay(5);

    return Ok();
}

This is where I login the user in my own app

Pieterjan
  • 2,738
  • 4
  • 28
  • 55
  • Just tried your solution, now all endpoints response has the `XSRF-TOKEN` set, however, the endpoint that I decorated with `[ValidateAntiForgeryToken]` failed with a response 400. Is the [Authorize] necessary? Also, is the Path='/' necessary. – For Comment Sep 07 '21 at 21:13
  • I've checked in chrome, under Cookies, it has `XSRF-TOKEN`, `.AspNetCore.Identity.Application`, `.AspNeCore.Antiforgery.rQWsmpBETsk` entries. These same entries also are present in the headers of that failed request. I thought the request header should have `X-XSRF-TOKEN` as specified in Startup, but it has `XSRF-TOKEN` instead, do I have to manually set it in the request header, i.e. `X-XSRF-TOKEN`? – For Comment Sep 07 '21 at 22:35
  • No, the name of the cookie you have is correct. Normally step 2 should make sure the `X-XSRF-TOKEN` header is sent in the request headers. Can you see the header being sent along in the **Network** tab? Can you see errors in the **Output** window? – Pieterjan Sep 08 '21 at 06:05
  • 1
    Yeah I did check in the Network tab via inspection, there is no `X-XSRF-TOKEN` header entry in the request headers, just those 3 listed above. The only "error" is the 400 response failure, no other error. I read somewhere Angular will not add that header entry automatically. So, I wonder if I need to manually append onto an http request? I am researching... – For Comment Sep 08 '21 at 15:45
  • Normally not. Step 2 is the only thing I did to get it to work. Sure you imported the `HttpClientXsrfModule` with its configuration? – Pieterjan Sep 08 '21 at 16:16
  • 1
    Yes. Here in my app.module.ts: `import { HttpClientModule, HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http';` and imports: `HttpClientXsrfModule.withOptions({ cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN' }),` – For Comment Sep 08 '21 at 16:18
  • You're right, I had the same issue. [Here is the solution](https://stackoverflow.com/a/59586462/8941307). The url must either be an absolute url (no hostname) (`/my/url`) or starting with `//example.com/my/url`. Omit the scheme. – Pieterjan Sep 08 '21 at 16:59
  • I just tried with 'api/endpoint' http request, still there is no `X-XSRF-TOKEN` entry in the request header – For Comment Sep 08 '21 at 19:14
  • Sadly that's all I can say. These steps worked for me. We will need a code repository to figure out what's going wrong. – Pieterjan Sep 08 '21 at 19:25
  • Have you tried also with `/api/endpoint` and `//localhost:port/api/endpoint` (fill out `port`)? – Pieterjan Sep 10 '21 at 07:11
0

Angular provides built-in enabled by default anti CSRF/XSRF protection.

Angular's HttpClient has built-in support for the client-side half of this technique. Read about it more in the HttpClient guide

Note that the CSRF/XSRF protection is enabled by default on the HttpClient but only works if the backend sets a cookie named XSRF-TOKEN with a random value when the user authenticates.

  • I read from that `HttpClient` guide, it says: By default, an interceptor sends this header on all mutating requests (such as POST) to relative URLs, but not on GET/HEAD requests or on requests with an absolute URL. So, this seems to say that by default Angular will NOT insert that anti-forgery header entry in most http requests, except POST on relative URLs. I am surprised that it didn't offer a way to manually append that header entry, or did it? I am confused. – For Comment Sep 08 '21 at 15:53