When deciding how to secure a Web Api there are a few choices available, for example you can choose to use JWT tokens or with a little bit less effort (but with other trade-offs), cookies.

If you decide to go with cookies and if your web api is consumed through a web application (e.g. Angular) it will be vulnerable to cross-site request forgery attacks (frequently referred to as CSRF or XSRF).

You can find the sample project for this blog post here in github.

What is CSRF and how it can be mitigated

To understand how CSRF works you need to understand how browsers handle cookies.

Every time you visit a website (e.g. mywebsite.com) and that website sends a response with a header named Set-Cookie a cookie is created. An example of a header that creates a cookie named myCookie with the value myCookieValue is: Set-Header: myCookie=myCookieValue .

Usually cookies are stored in a file on disk. This actually depends on the browser you are using but that’s essentially all they are. That is also the reason why you can log in to the same website with two different accounts if you use different browsers. Your “identity” is stored in a cookie and each browser can hold a different one for the same website.

Also, a cookie is specific to one website. That means that a cookie for example.com is not valid for foo.com . This boils down to the browser exclusively sending cookies to example.com that were created in a response to a request to example.com .

The way the browser “sends” the cookies is by including a header named Cookie whenever there is a request to the website from which the cookie originated.

For example if the browser has a cookie for example.com and you have a bookmark for example.com/homepage and you click it, the browser will automatically include a cookie header with the cookie value (e.g.: Cookie: myCookie=myValue ). There are exceptions to this namely the use of the SameSite policy. However this feature is still “experimental” and for the use case of allowing your web api to be potentially used by anyone, using a SameSite policy would not work.

The abuse of this mechanism (i.e. the browser sending the cookies automatically) is what CSRF exploits.

Here’s a simple example. Imagine your website has an endpoint at example.com/logout that responds to GET requests (which is a bad idea to begin with, but bear with me). Picture another website, named evilwebsite.com , that has an image element like this:

<img src="www.example.com/logout">

Just by visiting evilwebsite.com you’re logged out of example.com . This works because when the browser parses the html for evilwebsite.com and finds the img tag, it will perform a request to example.com/logout which will, for all intents and purposes, look like any other authenticated request from the point of view of example.com .

If you have a very popular website, or if your attacker has a way to target users of your website s/he can try to lure your users to a malicious website that can then perform requests to example.com as you.

Imagine if your bank website had this vulnerability. A malicious website could trigger transfers of money from your account to another account.

That’s really scary but thankfully it’s not too hard to mitigate. And we’ll even use cookies to mitigate it, here’s how.

CSRF mitigation

We’ve already seen that the browser will send the cookies it has for a website automatically. If any of those cookies is used to identify the user that cookie is usually set as an httponly cookie.

An httponly cookie is a cookie that is created using the httponly directive, for example:

Set-Cookie: AuthCookie=1Wkc5dGNtRnVaRzl0Y21GdVpHOXQ=; HttpOnly

This makes the cookie unavailable through JavaScript, i.e. if you run document.cookie that cookie won’t be visible.

It’s important that cookies that identify the user are httponly so that in case of a Cross-Site Scripting vulnerability (XSS) the attacker won’t be able to steal the auth cookie.

Now that we’ve established that we can create httponly cookies, let’s explore the fact that non- httponly are accessible via JavaScript.

Image that you’ve created a non httponly cookie with a random number, e.g. AntiForgeryCookie=42289347 and when you perform a request to the website, you read the cookie value in JavaScript and put it in a header in the request, e.g. AntiForgeryHeader: 42289347 .

When handling the request in the server you check if the cookie and the header values are the same. If they aren’t, or they are missing, the request is rejected.

This breaks things from the point of view of the attacker.

When an attacker triggers requests to a website where a user is logged in to, the auth cookie and the anti-forgery cookie are both sent but the attacker has no way to access them. It’s not possible to read the anti-forgery cookie’s value and put it in the header of that request (JavaScript running in evilwebsite.com cannot access your website).

In a nutshell that’s how CSRF is mitigated.

Applying CSRF mitigations in a Web Api built using ASP.NET Core

The out of the box functionality provided in ASP.NET Core for mitigating CSRF (named anti forgery) is geared towards Razor views.

You’ve probably seen it in the form of the @Html.AntiforgeryToken() html helper in previous versions of MVC (pre Core 2.0).

When you use the @Html.AntiforgeryToken() html helper in a Razor view a cookie is created alongside a hidden form field named __RequestVerificationToken . The value of both must match or else the request is rejected.

Thankfully the anti forgery features in ASP.NET Core are configurable enough that we can use them for a Web Api.

The first thing we have to do is to register the anti forgery dependencies and configure it so that instead of expecting a form field on POST requests, it expects a header. We can pick a name for our header, for example X-XSRF-TOKEN .

Here’s how that looks like in Startup.cs ‘s ConfigureServices method:

public void ConfigureServices(IServiceCollection services) { services.AddAntiforgery(options => { options.HeaderName = "X-XSRF-TOKEN"; }); //...

Specifying a HeaderName when configuring AntiForgery causes the anti forgery validation to use the header (instead of a form field named __RequestVerificationToken ) in the verification process.

You can also specify the cookie name you wish to use (e.g. options.Cookie.Name="MyAntiForgeryCookieName" ). This isn’t the cookie we’ll access through JavasScript though, so there’s no real advantage in doing this.

Now, for the part of actually generating the anti forgery cookies I recommend doing that in a controller action that you call immediately after a successful login. This might seem odd, however for the sake of brevity I’ll defer the reasoning behind this choice for later in this post.

Here’s how a controller action for generating the anti forgery cookies looks like:

[ApiController]

public class AntiForgeryController : Controller

{

private IAntiforgery _antiForgery;

public AntiForgeryController(IAntiforgery antiForgery)

{

_antiForgery = antiForgery;

}

[Route("api/antiforgery")] [IgnoreAntiforgeryToken] public IActionResult GenerateAntiForgeryTokens() { var tokens = _antiForgery.GetAndStoreTokens(HttpContext); Response.Cookies.Append("XSRF-REQUEST-TOKEN", tokens.RequestToken, new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false }); return NoContent(); }

}

First, we grab hold of the IAntiforgery service as a dependency and we use its GetAndStoreTokens method to actually generate the tokens/cookie values.

The GetAndStoreTokens method not only creates the cookie and the request tokens, it modifies the response so that the Set-Cookie statement is added to it (that’s why it needs HttpContext as an argument).

The Cookie and Request tokens are the terms used to refer to the two values that must match. The Cookie token is the one stored in the cookie and the Request token is either sent in a hidden form field __RequestVerificationToken or in a header value like we will do shortly.

You might be wondering why we need a Cookie and Request token if we only need one value to match (the value form the cookie and the value from the header) to mitigate CSRF. The reason for this is that the RequestToken contains the logged in username ( HttpContext.User.Identity.Name ) as well as a random value that matches the value stored in the cookie.

Here’s the discussion around this on github.

This is also the reason why I recommend using Antiforgery this way, in a separate request after the user logs in.

If we were to generate the anti forgery tokens in the response to the login request, the Request token that would be created would not contain a Username ( HttpContext.User.Identity.Name isn’t set yet at that time). Any subsequent requests that requires anti forgery validation would fail. That’s because even though the value in both anti forgery tokens would match, the username in the request token (empty) would not match HttpContext.User.Identity.Name .

Going back to the code, the next line is the non- httponly cookie that we will read using JavaScript and put in the requests we perform to the server:

Response.Cookies.Append("XSRF-REQUEST-TOKEN", tokens.RequestToken, new Microsoft.AspNetCore.Http.CookieOptions{ HttpOnly = false });

I’ve named it XSRF-REQUEST-TOKEN . This name has no effect on any Web Api code, so you can name it whatever you like. We’ll only access its value in JavaScript.

One final note about this controller action. It’s annotated with the IgnoreAntiforgeryToken attribute. This is one of the three attributes available in ASP.NET Core for dealing with CSRF, the other two are AutoValidateAntiforgery and ValidateAntiforgery .

ValidateAntiforgery will validate every request, whereas AutoValidateAntiforgery will only perform validation for unsafe HTTP methods (methods other than GET, HEAD, OPTIONS and TRACE).

These last two attributes are usually applied as global filters. In this case I recommend that you use the ValidateAntiforgery attribute since we want all requests to be validated.

The reason for this is that if we are relying on Cookies as our means to authenticate users, and we want consumers of our api to potentially come form any origin, we need a CORS configuration that is very permissive (see Secure an ASP.NET Core Web Api using Cookies).

With such a permissive configuration it is possible to forge GET requests from another domain and steal sensitive information, therefore they should also be checked for CSRF.

Here’s how we can configure all controller actions to be checked for CSRF:

public void ConfigureServices(IServiceCollection services) { //... services.AddMvc(options => { options.Filters.Add(new ValidateAntiForgeryTokenAttribute()); }); //...

We need to annotate some controller actions with IgnoreAntiforgeryToken . Namely the one that handles the user’s login, the one we’ve seen above that handles the anti forgery tokens’ generation and any others that you might want to make available without requiring the user being authenticated.

The client

In the client we need to make a request to the endpoint where the anti forgery tokens are generated after the user has logged in successfully.

Since the example project was done in Angular, where’s how that looks like in Angular:

//... export class AccountService { constructor(private httpClient: HttpClient) { } //... login(email: string, password: string) { return this.httpClient.post(`${environment.apiBaseUrl}/api/account/login`, { email, password }).pipe( switchMap(_ => this.httpClient.get(`${environment.apiBaseUrl}/api/antiforgery`)) ); }

This just performs two http request, one POST to api/account/login with the user’s credentials and a GET request to /api/antiforgery after the first request finishes successfuly.

We also need to read the cookie that is non- httponly ( XSRF-REQUEST-TOKEN ) and put it in a header named X-XSRF-TOKEN (that’s the name we used when configuring Antiforgery in Startup.cs ) when the client performs a request.

We could do that manually for every request, but that wouldn’t be very practical. Thankfully there are ways to have it be done automatically for every request (the beforeSend function in jQuery’s $.ajax for example).

If you are using Angular there’s an out of the box module HttpClientXsrfModule for that. Unfortunately, it does not work for cross domain requests. So what we’ll do here is add an http interceptor that reads the cookie and adds its value to outgoing requests as a header. Here’s how that looks like:

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; @Injectable() export class AddCsrfHeaderInterceptorService implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { var requestToken = this.getCookieValue("XSRF-REQUEST-TOKEN"); return next.handle(req.clone({ headers: req.headers.set("X-XSRF-TOKEN", requestToken) })); } private getCookieValue(cookieName: string) { const allCookies = decodeURIComponent(document.cookie).split("; "); for (let i = 0; i < allCookies.length; i++) { const cookie = allCookies[i]; if (cookie.startsWith(cookieName + "=")){ return cookie.substring(cookieName.length + 1); } } return ""; } }

An http interceptor is a normal service but needs to be registered in a module using a "multi" provider (you can have several of them), here's how that looks like:

@NgModule({ //... providers: [ //... { provide: HTTP_INTERCEPTORS, useClass: AddCsrfHeaderInterceptorService, multi: true } ] //...

That's it, your api is now safe form CSRF attacks.

If you had some issues with the example or have questions please write them down in the comments and I'll get back to you as soon as I can.

It's only fair to share... Linkedin