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

2 Answers 2

3

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

Sign up to request clarification or add additional context in comments.

10 Comments

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.
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?
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?
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...
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' }),
|
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.

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.