Cross-site request forgery (also known as XSRF or CSRF) is an common attack against web apps that store authentication tokens in the cookies. Browser will automatically attach these authentication cookies with every request to the website.
There are two options to mitigate XSRF attack:
- Use token based authentication instead of cookie based authentication. That means after user is authenticated, the authentication token is stored in browser’s local/session storage (instead of cookie). In each subsequent request, the token is passed in the request header for server-side validation. This approach is growing more and more popular in modern Single Page Application (SPA).
- Use anti-forgery token to protect the request forgery. We will explain in more detail how it works in ASP.NET core.
AntiforgeryTokenSet: RequestToken and CookieToken
In ASP.NET core, anti-forgery token set contains two tokens: one is called RequestToken and another is CookieToken. Both tokens have a string property called SecurityToken, which is random generated string. CookieToken, as its name indicates, is stored in a cookie. The RequestToken can be stored in either a form field or HTTP header. ASP.NET core will match the SecurityToken stored in both CookieToken and RequestToken.
ASP.NET core also validates the user information inside RequestToken against the current user in HTTP request. Therefore, after user is successfully authenticated, we should issue a new set of anti-forgery tokens.
Anti-forgery options
Customize anti-forgery options in Startup.ConfigureServices
:
services.AddAntiforgery(options =>
{
options.Cookie.Name = "AntiforgeryCookieName";
options.FormFieldName = "AntiforgeryFieldName";
options.HeaderName = "AntiforgeryHeaderName";
options.SuppressXFrameOptionsHeader = false;
});
The default value of these options are:
Cookie.Name: “.AspNetCore.Antiforgery.” plus the first 8 bytes of the hash value of ApplicationDiscriminator. The cookie generated has the flag “samesite=strict; httponly”. Therefore, javascript cannot access the cookie.
FormFieldName: __RequestVerificationToken
HeaderName: RequestVerificationToken. If the value is null, anti-forgery validation will only consider form data. That is, validate the token stored in the form field and the value in the cookie.
Razor Pages are automatically protected from XSRF/CSRF by using form field and cookie. The FormTagHelper injects anti-forgery request token into HTML form element. If you need explicitly add an anti-forgery request token to a <form>
element, use HTML helper @Html.AntiForgeryToken.
ASP.NET Core adds a hidden form field to the form similar to the following:
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">
Note: this is the RequestToken we have discussed above. The CookieToken would be stored in .AspNetCore.Antiforgery.xxxxxxxx.
AJAX
In modern JavaScript-based apps and SPAs, many requests are made programmatically. These AJAX requests will use request headers and cookies to send the anti-forgery tokens.
We need a way to pass the RequestToken to front end so front end can attach it to the HTTP header defined in anti-forgery option HeaderName. We normally generate the anti-forgery token set and send the RequestToken back to client by cookie.
Angular $http
service uses a convention to address CSRF. If the server sends a cookie with the name XSRF-TOKEN
, the $http
service adds the cookie value to a header called X-XSRF-TOKEN
when it sends a request to the server. This process is automatic. The header doesn't need to be set in the client explicitly.
We could configure ASP.NET core to meet this convention:
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
app.Use(next => context =>
{
string path = context.Request.Path.Value; if (
string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
{
// The request token can be sent as a JavaScript-readable cookie,
// and Angular uses it by default.
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
} return next(context);
});
}public void ConfigureServices(IServiceCollection services)
{
// Angular's default header name for sending the XSRF token.
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
}
antiforgery.GetAndStoreToken() call would generate the anti-forgery token set and save the CookieToken into .AspNetCore.Antiforgery.xxxxxxxx cookie. The statement next line would save the RequestToken to a JavaScript readable cookie called “XSRF-TOKEN”. Angular then can read the RequestToken from “XSRF-TOKEN” cookie and add it to the HTTPX-XSRF-TOKEN
header.
Single Page Applications (SPA)
SPA application normally has front end HTML + Javascript and backend APIs. Protecting these APIs against anti-forgery attack may not be very straight forward.
The best way to protect the API in SPA architecture is to use token based authentication, such as JWT. In this case, we do not need to use anti-forgery token at all.
If you have to use cookie based authentication, the API has to be hosted in the same site as front end in order for it to work. The CookieToken stored in .AspNetCore.Antiforgery.xxxxxxxx cookie is marked by the framework as samesite=strict and httponly. So if the front end app and API are hosted in different domain, the browser would not send the cookie. Thus the anti-forgery validation would fail.
Even if the front end and API are hosted in same domain, there are still two things to consider:
- Before the anti-forgery validation, the anti-forgery cookie token and request token has to be sent to front end. Since SPA application call API using AJAX, the tokens have to be sent in some API calls before the call that need to be validated. If the order of API calling is not predictable, you may end up validate the API call without valid tokens.
- Because every time the user information changes (login, impersonate,etc.), the system needs to issue a new set of anti-forgery token. The tokens cannot be issued in login or impersonate method since the HttpContext.User is still the old one. The HttpContext.User only changes in the next API call.
The most practical way to solve these issues are expose an API method to create the anti-forgery cookies. Front end would have to call this API method first before submitting the request that needs the anti-forgery validation.