'Apparently Random Error: "Antiforgery token validation failed. The antiforgery cookie token and request token do not match."

Background

I have a relatively new ASP.NET Core 2 site. It's running on just one server (Windows Server 2012 R2, IIS 8.5), and I only restart the site once every few days when I upload an update. About once a day, a user's request fails due to rejection by the anti-forgery system. These are POST requests, and there's nothing particularly special about them. I'm including the anti-forgery value in the POST request, and 99% of the time, POST requests work. But when they don't, the stdout log says, "Antiforgery token validation failed. The antiforgery cookie token and request token do not match." When I perform a Web search using that exact statement, I get zero results. So I've turned to Stack Overflow. [This is no longer true as a Web search now yields this Stack Overflow question.]

Errors

I've included the relevant portions of the stdout log below.

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 POST [domain redacted] application/x-www-form-urlencoded 234
info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ValidateAntiforgeryTokenAuthorizationFilter[1]
      Antiforgery token validation failed. The antiforgery cookie token and request token do not match.
Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The antiforgery cookie token and request token do not match.
   at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery.ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet)
   at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery.<ValidateRequestAsync>d__9.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ValidateAntiforgeryTokenAuthorizationFilter.<OnAuthorizationAsync>d__3.MoveNext()
info: Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker[3]
      Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.AutoValidateAntiforgeryTokenAuthorizationFilter'.
info: Microsoft.AspNetCore.Mvc.StatusCodeResult[1]
      Executing HttpStatusCodeResult, setting HTTP status code 400
info: Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker[2]
      Executed action /Index in 2.6224ms
warn: Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery[1]
      Antiforgery validation failed with message 'The antiforgery cookie token and request token do not match.'.

For requests that result in the above stdout output, IAntiforgery.IsRequestValidAsync agrees by returning false. Notice the error message "The antiforgery cookie token and request token do not match." Here's a reduced example of a failed POST request and the associated cookie.

POST: __RequestVerificationToken= CfDJ8F9Fs4CqDFpLttT96eZw9WHjWfHO8Yawn35k4Yq3gDK5n1TDJDDiY5o86VQs1_qOVIYBydCizBU4knb7Jmq1-heGhwnMu2KmhUIiAd0xI7Sudv3GX-J0OI6wRfiPL4L1KRs2Pml8dbsDfwemewBqi18

Cookie: .AspNetCore.Antiforgery.ClRyCRmWApY=CfDJ8F9Fs4CqDFpLttT96eZw9WFtJht41WcNrmgshi2pFGwcxhr0_0hvINQc7Yl9Cbjhv-TiSNXeEctyKborLI49AcjHfWIgOmmKkbjOe7QMn8Z0WZtkQy5JcaBHKEGTu1p-La8JL8pZZqZy02Hrswpkh3I

I've also captured this data a few times after the request has failed with a 400 error (using some error handling middleware):

AntiforgeryTokenSet tokens = antiforgery.GetTokens(context);
tokens.CookieToken:  null
tokens.FormFieldName:  "__RequestVerificationToken"
tokens.HeaderName:  "RequestVerificationToken"
tokens.RequestToken:  "CfDJ8F9Fs4CqDFpLttT96eZw9WH33jSw5mM8h7RpEd3vGISQTRkx1rfwm-L2lfkvXKMBc-riESmoTo_fnIjeBbRmOo5KuJHr09f8B75sQ9g_djIVeeaGwMw5KE6W1O2-7Vi03fCnwlTv8l-BWGst76Ln-ZQ"

So here are the three strings:

POST String:  "CfDJ8F9Fs4CqDFpLttT96eZw9WHjWfHO8Yawn35k4Yq3gDK5n1TDJDDiY5o86VQs1_qOVIYBydCizBU4knb7Jmq1-heGhwnMu2KmhUIiAd0xI7Sudv3GX-J0OI6wRfiPL4L1KRs2Pml8dbsDfwemewBqi18"
Cookie String:  "CfDJ8F9Fs4CqDFpLttT96eZw9WFtJht41WcNrmgshi2pFGwcxhr0_0hvINQc7Yl9Cbjhv-TiSNXeEctyKborLI49AcjHfWIgOmmKkbjOe7QMn8Z0WZtkQy5JcaBHKEGTu1p-La8JL8pZZqZy02Hrswpkh3I"
antiforgery.GetTokens(context).RequestToken:  "CfDJ8F9Fs4CqDFpLttT96eZw9WH33jSw5mM8h7RpEd3vGISQTRkx1rfwm-L2lfkvXKMBc-riESmoTo_fnIjeBbRmOo5KuJHr09f8B75sQ9g_djIVeeaGwMw5KE6W1O2-7Vi03fCnwlTv8l-BWGst76Ln-ZQ"

The POST string and cookie string don't match, but in my experience, even with requests ASP.NET Core considers legitimate, they never do. But strangely, the POST string and tokens.RequestToken don't match either. I would think they should match, although I captured tokens.RequestToken later in the request lifecycle, so maybe that has something to do with it.

ASP.NET Core 2 on GitHub

I decided to look at the source code of ASP.NET Core 2. I found this file, especially line 145:

https://github.com/aspnet/Antiforgery/blob/dev/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs

That line gets the message "The antiforgery cookie token and request token do not match." from this file at line 134:

https://github.com/aspnet/Antiforgery/blob/dev/src/Microsoft.AspNetCore.Antiforgery/Resources.resx

So I think that's where the message is originating, but I'm still left wondering why this is happening.

Question

Would someone please help me figure out why these anti-forgery tokens aren't validating? Is it possible the user's Web browser is mangling the cookie or POST data? Does anyone have experience in this area or any suggestions? Thank you.



Solution 1:[1]

Disabling the filter globally seems to be the only way to turn it off. I got @svallis's code to work with a sight modification:

services.AddMvc().AddRazorPagesOptions(options =>
{
    options.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute());
});

https://github.com/aspnet/Mvc/issues/7012

Solution 2:[2]

I found solution here: https://github.com/aspnet/Antiforgery/issues/116

using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;


// fix from https://github.com/aspnet/Antiforgery/issues/116
namespace WebAppCore.Code
{
    public class HtmlGeneratorNoStoreAntiforgery: DefaultHtmlGenerator  
    {
        public HtmlGeneratorNoStoreAntiforgery(
            IAntiforgery antiforgery,
            IOptions<MvcViewOptions> optionsAccessor,
            IModelMetadataProvider metadataProvider,
            IUrlHelperFactory urlHelperFactory,
            HtmlEncoder htmlEncoder,
            ValidationHtmlAttributeProvider validationAttributeProvider)
            : base(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory, htmlEncoder, validationAttributeProvider)
        {
        }

        public override IHtmlContent GenerateAntiforgery(ViewContext viewContext)
        {
            var result = base.GenerateAntiforgery(viewContext);

            viewContext
                    .HttpContext
                    .Response
                    .Headers[HeaderNames.CacheControl]
                = "no-cache, max-age=0, must-revalidate, no-store";

            return result;
        }
    }
}

and add in startup.cs:

services.AddSingleton<Microsoft.AspNetCore.Mvc.ViewFeatures.IHtmlGenerator, HtmlGeneratorNoStoreAntiforgery>();

Solution 3:[3]

Regarding CookieToken to be null: Maybe that request was forged? Since the cookie is missing and I suspect that your website DOES send it every request. When can it be missing? When it's coming from somewhere else.

Regarding the other issue:

  1. Is your middlewhere, where the AntiforgeryToken is added as a cookie on the response after the authentication part of the Configure method?
  2. Is it possible that it might be that you're returning several AntiforgeryTokens on several GET-requests? In this case there might be a race condition which request comes back the latest (what the browser will use) and which request left the server the last; these might be different -> token mismatch.

You should return just one, at the first Get operation. When you're hosting the Frontend and Backend on one instance it will probably be the root (/) and if you're using an API on another port you should make sure that you do a GET before a POST, PUT or PATCH.

One instance startup example:

builder.Use(next => context =>
{
    var path = context.Request.Path.Value;
    if (!string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
        context.Request.Method != "GET") return next(context);
    var tokens = antiforgery.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
        new CookieOptions { HttpOnly = false, Path = "/" });

    return next(context);
});

Multiple ports instance (mind the CORS)

public IServiceProvider ConfigureServices(IServiceCollection services)
{
  services.AddCors(options =>
  {
     options.AddDefaultPolicy(builder => builder
            .WithOrigins(/*allowed domains here*/)
            .AllowAnyHeader()
            .WithMethods("GET", "POST", "PUT", "DELETE")
            .AllowCredentials()
            .Build()
     );
  });
}
public virtual void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
  app.UseAuthentication()
     .UseAuthorization()
     .Use(next => context =>
      {
         var path = context.Request.Path.Value;
         if (!string.Equals(path, "/api/settings", StringComparison.OrdinalIgnoreCase) ||
             context.Request.Method != "GET") return next(context);
         var tokens = antiforgery.GetAndStoreTokens(context);
         context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
             new CookieOptions { HttpOnly = false });

         return next(context);
      });
}

Bear in mind that if you're hosting the Frontend on another port/location you should add the header for withCredentials like this:

return this.http.get<Settings>(`/api/settings`, { withCredentials: true });

Solution 4:[4]

I was looking for a solution on a new .NET 6.0 Blazor Server App with Authentification and fixit like the MSDN without disable Antiforgery:

 app.UseRouting();

app.UseAuthorization();

var antiforgery = app.Services.GetRequiredService<IAntiforgery>();

app.Use((context, next) =>
{
    var requestPath = context.Request.Path.Value;

    if (string.Equals(requestPath, "/", StringComparison.OrdinalIgnoreCase)
        || string.Equals(requestPath, "/index.html", StringComparison.OrdinalIgnoreCase))
    {
        var tokenSet = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken!,
            new CookieOptions { HttpOnly = false });
    }

    return next(context);
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 user5919789
Solution 2 Pavel Samoylenko
Solution 3
Solution 4 Wolfgang Schorge