Search code examples
c#authenticationcookiesasp.net-core-2.2antiforgerytoken

Antiforgery exception (Provided antiforgery token meant for a different claims-based user) on post method, when user is authenticated


I am using ASP.NET Core 2.2 with AspNetCore.Authentication.

I am trying to use Post call in my cshtml _Layout's view component's form:

<form asp-controller="Home" asp-action="SearchResults" method="post" enctype="multipart/form-data" asp-antiforgery="true" novalidate>

(...)

</form>

My intetntion is to mask URL with search properties to make web crawling and scraping more difficult. My Post method is updating static fields of my controller, and redirecting to my Index Get method, where are displayed search results:

    private static SearchViewModel _searchModel;
    private static string _sortOrder;
    private static int? _pageNumber;

(...)

[HttpPost]
        [AllowAnonymous]
        public IActionResult SearchResults(SearchViewModel searchModel, string sortOrder, int? pageNumber = 1)
        {
            _searchModel = searchModel;
            _sortOrder = sortOrder;
            _pageNumber = pageNumber;

            return RedirectToAction(nameof(Index));
        }

And Get method signature:

[HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> Index()
{
    //refering to updated fields
}

Idea inspired with this SO reply.

And Setup.cs identity/authentication setup:

public void ConfigureServices(IServiceCollection services)
        {
             services.AddIdentity<AppUser, IdentityRole>(opts => {
                opts.User.RequireUniqueEmail = true;
                opts.User.AllowedUserNameCharacters = null; //disable validation
                opts.Password.RequiredLength = 8;
                opts.Password.RequireNonAlphanumeric = false;
                opts.Password.RequireLowercase = false;
                opts.Password.RequireUppercase = false;
                opts.Password.RequireDigit = true;
            })
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();
            services.AddAntiforgery(x => x.HeaderName = "X-CSRF-TOKEN");
            .Services.ConfigureApplicationCookie(options =>
            {
                //previous cookies not valid
                options.SlidingExpiration = true;
                options.ExpireTimeSpan = TimeSpan.FromDays(1);
            });
            services.AddMvc(options =>
            {
                options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
                options.Filters.Add<AuditUserActionFilter>();
            });
            
            (...)
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAntiforgery antiforgery)
        {
            (...)
            
            app.Use(next => context =>
            {
                if (context.Request.Path == "/")
                {
                    var tokens = antiforgery.GetAndStoreTokens(context);
                    context.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false });
                }
                return next(context);
            });
            app.UseAuthentication();
        }
    }

I belive that this is a key: Actually, when I am submitting Post form as a guest (not authenticated), everything works fine. But after signing in, on form submit I am receiving an exception:

Provided antiforgery token meant for a different claims-based user

Also please find below my user authentication process:

if (user != null)
                {
                    await _signInManager.SignOutAsync();

                    var result = await _signInManager.PasswordSignInAsync(user, details.Password, false, false);

                    if (result.Succeeded)
                    {
                        if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
                            return Redirect(returnUrl);
                        else
                            return RedirectToAction(nameof(Account));
                    }
                    else if (result.IsLockedOut || result.IsNotAllowed)
                    {
                        ViewBag.Message = "You are not allowed to sign in.";
                        return View("Error");
                    }
                    else
                    {
                        ModelState.AddModelError(nameof(UserLoginViewModel.Password), "Incorrect password.");
                        return View("Index");
                    }
                }

I was trying several approaches from here, and here with no results. But they are for older versions of ASP.NET Core. I am not sure how I am supposed to update my antiforgery tokens for authenticated user?

Possible reasons of my problem:

  1. misunderstanding of Post -> Get form call.
  2. not understanding how authentication cookies works in ASP.NET Core authentication.
  3. misunderstanding of authentication process in general.

P.S. When I remove

options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());

from:

services.AddMvc(options =>
            {
                options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
                options.Filters.Add<AuditUserActionFilter>();
            });

I am able to make Post call. But I belive, that it is not any surprise.

EDIT: Because of some reason, when I submit form from another view (form is in _Layout's view component) than Home/Index (where I display search results), there is no antiforgery exception.

Is it because HomeController's Index action is Get? After adding Post / Get attribute:

[AllowAnonymous]
        [HttpGet, HttpPost]
        public async Task<IActionResult> Index()

I have same exception as before. After adding:

[AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Index()

I am not able to load the view at all, because it is asking for the token.

How to bypass it?


Solution

  • After itminus advice I understood, that I have to move in my Configure method my app.UseAuthentication(); to the top. Also, after reading documentation I realized, that after using AddMvc I do not need anymore code refering to "CSRF-TOKEN".

    At the end my Startup.cs looks like:

    public void ConfigureServices(IServiceCollection services)
            {
                 services.AddIdentity<AppUser, IdentityRole>(opts => {
                    opts.User.RequireUniqueEmail = true;
                    opts.User.AllowedUserNameCharacters = null; //disable validation
                    opts.Password.RequiredLength = 8;
                    opts.Password.RequireNonAlphanumeric = false;
                    opts.Password.RequireLowercase = false;
                    opts.Password.RequireUppercase = false;
                    opts.Password.RequireDigit = true;
                })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
                .Services.ConfigureApplicationCookie(options =>
                {
                    //previous cookies not valid
                    options.SlidingExpiration = true;
                    options.ExpireTimeSpan = TimeSpan.FromDays(1);
                });
                services.AddMvc(options =>
                {
                    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
                    options.Filters.Add<AuditUserActionFilter>();
                });
    
                (...)
            }
    
            public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAntiforgery antiforgery)
            {
                app.UseAuthentication();
    
                (...)
            }
        }