Anti-Forgery Tokens were introduced in ASP.NET in order to prevent Cross-Site Request Forgeries. There are many sites which describe how to use and configure those tokens in your application. But in this post I’m going to show you what exactly those tokens contain, where they are generated and how to customize them.
Let’s start our journey from a sample Razor HTTP form:
...
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.TextBoxFor(m => m.Name)<br />
@Html.TextBoxFor(m => m.FullName)<br />
<br />
<input type="submit" value="Test" />
}
...
As you can see we are generating the token when the page is rendered. In the browser it appears as just another hidden input in the form:
<input name="__RequestVerificationToken" type="hidden" value="i411mJIr0mZKrk17g4Hf-0_G6aXOJLkzzGfd5yn2mVsTqj-35j_n0YUUCzFRXoFet3BXUVpBicpL3p-AqPPA3XEXEtykt4X-_MbRIxLQH6M1" />
Also if you look at the cookies set on the server response you should see a cookie with a name starting from __RequestVerificationToken:
The value from the input field (called Form Token) and from the cookie (called Cookie or Session Token) are correlated and both are required for a successful request validation. The System.Web.Helpers.AntiForgery
class is used to generate both those tokens. This class under the hood uses System.Web.Helpers.AntiXsrf.AntiForgeryWorker
and creates an instance of it at the initialization:
private static readonly AntiForgeryWorker _worker = CreateSingletonAntiForgeryWorker();
private static AntiForgeryWorker CreateSingletonAntiForgeryWorker()
{
IAntiForgeryConfig config = new AntiForgeryConfigWrapper();
IAntiForgeryTokenSerializer serializer = new AntiForgeryTokenSerializer(MachineKey45CryptoSystem.Instance);
ITokenStore tokenStore = new AntiForgeryTokenStore(config, serializer);
IClaimUidExtractor claimUidExtractor = new ClaimUidExtractor(config, ClaimsIdentityConverter.Default);
ITokenValidator tokenValidator = new TokenValidator(config, claimUidExtractor);
return new AntiForgeryWorker(serializer, config, tokenStore, tokenValidator);
}
One method in the AntiForgeryWorker
class is especially interesting for us: void GetTokens(HttpContextBase httpContext, AntiForgeryToken oldCookieToken, out AntiForgeryToken newCookieToken, out AntiForgeryToken formToken)
. As its name suggests, it is responsible for retrieving tokens for further processing. Under the hood it is using System.Web.Helpers.AntiXsrf.TokenValidator
to generate token values.
Cookie/Session Token
The Cookie Token contains a token version, randomly generated byte array (16 bytes long) called Security Token and a boolean flag set to 1 (which indicates it’s a session token). The Security Token array is then encrypted and encoded using Base64UrlToken encoding and stored in a session cookie with the HttpOnly attribute set (so you can’t access it from JavaScript code). Our sample cookie has a value of
Aq81hoVCPIpq3Q6xjBi0EFKKwSFwnKROgS7tyXF393eAN8rdMNZwkVkEgjQokKviKLVST1iWdgDxBt-g3FIughAsczUO7tyWhtz3fs88xMM1
which after decoding and decrypting
BitConverter.ToString(System.Web.Helpers.AntiXsrf.MachineKey45CryptoSystem.Instance.Unprotect("Aq81hoVCPIpq3Q6xjBi0EFKKwSFwnKROgS7tyXF393eAN8rdMNZwkVkEgjQokKviKLVST1iWdgDxBt-g3FIughAsczUO7tyWhtz3fs88xMM1"))
gives:
01-1A-CF-C9-ED-F1-3E-1E-7D-C9-9E-BE-90-2E-22-91-36-01
The token version is 0×1 (the first byte) and we can see that it’s a session token (the last byte).
The last thing I need to mention when describing Cookie Tokens is the name of the cookie which contains our token. It might be sometimes the source of troubles when we transfer anti-forgery tokens between applications (I will write about it later on). In our sample the cookie name is __RequestVerificationToken_L3NoYXJlZC1zZWN1cmVk0. If we decode the L3NoYXJlZC1zZWN1cmVk0 part (System.Text.Encoding.UTF8.GetString(HttpServerUtility.UrlTokenDecode("L3NoYXJlZC1zZWN1cmVk0"))
) we will receive: /shared-secured which is the name of a virtual directory my application runs in. If we would like to change the name of the cookie we can do so by adding the following code to, for instance, the Application_Start event handler:
AntiForgeryConfig.CookieName = "__RequestVerificationToken" + "_" + HttpServerUtility.UrlTokenEncode(Encoding.UTF8.GetBytes("/shared-secured"));
This might come in handy when you have two applications under different virtual directories (but under the same domain) and you would like them to share anti-forgery tokens.
Form Token
Compared to Cookie Tokens, Form tokens contain additionally information about the currently logged user as well as optional additional data. Let’s examine the Form Token from my sample output:
i411mJIr0mZKrk17g4Hf-0_G6aXOJLkzzGfd5yn2mVsTqj-35j_n0YUUCzFRXoFet3BXUVpBicpL3p-AqPPA3XEXEtykt4X-_MbRIxLQH6M1
After decoding and decrypting
BitConverter.ToString(System.Web.Helpers.AntiXsrf.MachineKey45CryptoSystem.Instance.Unprotect("Aq81hoVCPIpq3Q6xjBi0EFKKwSFwnKROgS7tyXF393eAN8rdMNZwkVkEgjQokKviKLVST1iWdgDxBt-g3FIughAsczUO7tyWhtz3fs88xMM1"))
we receive:
01-1A-CF-C9-ED-F1-3E-1E-7D-C9-9E-BE-90-2E-22-91-36-00-00-00-00
As you can see there are three additional bytes at the end: first byte is a flag desribing if the next field is a 256-bit claims uid – otherwise the next field is a username; second byte should be the first byte of either a claims uid or a username; third byte is an additional data string (empty in our case). When I generated the token I wasn’t authenticated in the application that’s why the username/claims uid byte is empty. Let’s see how this token will look like when a user is authenticated. I created a simple ASP.NET MVC app with the default authentication (based on ASP.NET IdentityModel). The generated token was as follows:
a3UA13kXQWK2rlYwCPk-jQdQiaz1aAIwDk0RQkgEsXrEv5j4HTbnH6LLqxKoXcLJ9CUcTs60sc4WmSMVfjDtD8fBx9NYh1qxlWZdxk1LY-UHhTRn97UN_HKCdJAZK5XtC130pbmFmuIOtirDSake_g2.
After decoding and decrypting we have:
01-D8-80-F5-14-1F-5B-ED-2E-6E-A5-9D-61-A4-7E-3E-14-00-01-60-C6-BF-36-93-9B-24-A7-55-49-70-0A-CB-05-31-29-8E-93-E6-5C-A5-33-EF-13-F6-92-8D-2F-B7-A7-05-24-00
I mark in bold the interesting part (the unbolded part as you remember is the encoded Security Token). The first byte of the bolded section indicates that next bytes contain a claims uid. The anti-forgery mechanism by default uses two claims:
I then serialized each string using BinaryWriter
, combined their binary representations together and computed SHA256 hash:
PS anti-forgery> .\BinarySerializer.exe test.bin http:
PS anti-forgery> [BitConverter]::ToString($a.ComputeHash([IO.File]::ReadAllBytes("test.bin")))
60-C6-BF-36-93-9B-24-A7-55-49-70-0A-CB-05-31-29-8E-93-E6-5C-A5-33-EF-13-F6-92-8D-2F-B7-A7-05-24
BinaryWriter.exe is a very simple application that generates a binary file from strings passed in arguments:
using System;
using System.IO;
public class Program
{
public static void Main(String[] args) {
if (args.Length < 2) {
Console.WriteLine("Usage: BinarySerializer.exe <output-file> <string1> [<string2> <string3> ...]");
return;
}
using (var fs = new FileStream(args[0], FileMode.Create)) {
using (var bw = new BinaryWriter(fs)) {
for (int i = 1; i < args.Length; i++) {
bw.Write(args[i]);
}
}
}
}
}
As you can see in the command line output our generated hash matches the one from the token. If you are not using default claims for authentication, you will need to define your unique claim type somewhere in your application initialization code:
AntiForgeryConfig.UniqueClaimTypeIdentifier = "urn:myuniqueidentityclaim"
The generated hash will then be built from two serialized strings: your claim type name and its value.
Common issues
The required anti-forgery cookie “__RequestVerificationToken_xxxxxxx” is not present.
This one is quite straightforward and indicates that there was no Cookie Token found that would match the Form Token sent in the request. Make sure that the application uses a valid cookie name (you can change it using AntiForgeryConfig.CookieName
) and the client has cookies enabled.
The anti-forgery token could not be decrypted. If this application is hosted by a Web Farm or cluster, ensure that all machines are running the same version of ASP.NET Web Pages and that the <machineKey> configuration specifies explicit encryption and validation keys. AutoGenerate cannot be used in a cluster.
This one is more tricky and it may indicate all kinds of problems. If you have two application sharing an Anti-Forgery token, make sure they have the same machineKey configuration and share the cookie name. If user is authenticated when sending the form, make sure you use the same identity claims in both applications. Finally, check if Security Token stored in a Cookie Token matches the one in a Form Token.
I hope information presented in this post will help you better understand ASP.NET anti-forgery tokens and make diagnosing anti-forgery issues easier :)
Filed under: ASP.NET Security, CodeProject