My team has recently launched our first .NET Core application into production on Linux.
It’s deployed in a load balanced configuration, which brings different considerations in .NET Core than it did in previous versions of ASP.Net.
In previous versions of .Net, session state could be shared via a single central node (StateServer), or in SQL Server. I had trouble with the performance of SQL Server session state solution in the past, and I’ve avoided it ever since. I’ve also seen the Forms Authentication mechanism’s sliding expiration being a problem highlighted on penetration tests, so I’ve tended to avoid the built-in solutions.
I tend to use a JWT as a session token now, since it doesn’t require a central server that all apps can access and it works well across multiple platforms. Support for this is good in .NET Core, except for the lack of built-in support for PEM files in the .NET Framework generally which meant I had to use external tools to convert.
Using JWT for session over Microsoft’s implementation has some cross-platform benefits. For example, part of the service my team has written is implemented in Go. The Go application can validate the session JWT produced by the .NET Core application without making any service calls or database calls (it’s stored in a cookie). This would have been a pain had we used a Microsoft-specific session solution and would have required that the Go application be able to read from a SQL Server database, or from Microsoft’s session state and share some codes.
There are two main ways to use a JWT, once is to use the RSA algorithm which allows an issuer of a JWT to sign the JWT with a private key. This allows any clients to validate JWTs they receive using a public key, but they would not be able to issue their own JWTs.
The other way is to use a symmetric algorithm - a shared secret - in which case any holder of the shared secret could also produce a signed JWT.
When using a JWT as an Authentication token across multiple applications, it makes sense to use the RSA algorithm: keep the private key secure and distribute the public key far and wide to each client application. In fact, I’ve never used a symmetric algorithm for signing JWTs.
CSRF protection (prevents HTTP form post submissions from external sources being accepted by your Website by setting a cookie and including a token in the HTML form) lends itself quite well to using a symmetric algorithm, since each Website in the farm needs to both generate tokens for other servers in the farm, and accept tokens from other servers in the farm.
In the old .NET world, you could modify the machine.config file on multiple machines to synchronise the machine keys used to validate viewstate etc. The machine key is a Symmetric encryption key, since each machine must be able to encrypt and decrypt data. In .NET Core, there is no machine.config, so what’s the solution now?
In .NET Core, the Antiforgery feature has been improved to automatically issue new keys every couple of days by using the Data Protection API. This is good, because if you lose your private key once, a new key will be issued fairly soon. The problem is that the machines need to share the key they’ve used with other machines.
The options are:
- Setup an NFS share between Windows machines (not doing that)
- Setup a Redis cluster to share state, configure firewall rules to permit it etc.
I was unimpressed because it adds a bunch of infrastructure to manage and a bunch of unknown and potentially complex failure modes and attack vectors: https://github.com/aspnet/Antiforgery/issues/143
As I say in the issue, other platforms are pretty simple and the implementations are easy to read and understand, e.g. the Gorilla implementation for Go https://github.com/gorilla/csrf
To avoid the infrastructure work and extra complexity, I wrote an implementation which uses a symmetric algorithm with a symmetric key. I can then provide each server with a copy of the shared secret. It lacks the ability to rotate keys, but if an attacker has the key, then I’ve been pwned and the CSRF protection is the least of my worries anyway. It’s also trivial for me to launch a new version with a new key.
You can check it out here.
I’ve also published it as a NuGet package to make it easy to use.
I’d guess not many people are using .NET Core in production yet. I expect people will be surprised when they find that they have extra work to do if they want to use the Antiforgery / CSRF feature.
Additionally, during testing, I discovered that I had assumed CSRF protection was enabled by default on all requests because HTML tag helpers introduce the HTML form elements and HTTP cookies. In fact, even though tokens are always produced, in order to validate those tokens, you have to apply an attribute to each controller action or use a global filter…