The customer eventually agreed that the Antiforgery implementation of ASP.NET is sufficient.
Just for fun I wanted to extend Antiforgery to meet the invalidation requirement.
The Antiforgery library has one extensibility point: IAntiforgeryAdditionalDataProvider
(Core) and IAntiForgeryAdditionalDataProvider
(pre-Core)
In ASP.NET MVC (pre-Core) you can set this on startup.
using System.Web;
using System.Web.Helpers;
// ...
namespace AntiForgeryStrategiesPreCore
{
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
// ...
AntiForgeryConfig.AdditionalDataProvider = new MyAdditionalDataProvider();
}
}
}
For ASP.NET Core you need to register your IAntiforgeryAdditionalDataProvider
as a service. If you don't it will use the DefaultAntiforgeryAdditionalDataProvider
(source) which does nothing.
using System;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace AntiForgeryStrategiesCore
{
// ...
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IAntiforgeryAdditionalDataProvider, SingleTokenAntiforgeryAdditionalDataProvider>();
// ...
}
}
// ...
}
Now you can add additional data to your Antiforgery token which will be encypted into your cookie and and form field.
Here's a MVC Core example that holds on to a single token in Session, and removes it after usage.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
namespace AntiForgeryStrategiesCore
{
public class SingleTokenAntiforgeryAdditionalDataProvider : IAntiforgeryAdditionalDataProvider
{
private const string TokenKey = "SingleTokenKey";
public string GetAdditionalData(HttpContext context)
{
var token = TokenGenerator.GetRandomToken();
context.Session.SetString(TokenKey, token);
return token;
}
public bool ValidateAdditionalData(HttpContext context, string additionalData)
{
var token = context.Session.GetString(TokenKey);
context.Session.Remove(TokenKey);
return token == additionalData;
}
}
}
This isn't recommended because when you open multiple tabs with multiple forms, only one of the forms will have the valid token in session, and the other will fail. That's why I made one that holds on to multiple tokens.
You can find that AdditionalDataProvider and others on GitHub (Time based, Queue based).