Search code examples
c#vue.jsasp.net-coreaxiosantiforgerytoken

.NET Core 3.1, Vue, Axios and [ValidateAntiForgeryToken]


I've been playing with this all day and reading as much as I can and I've utterly failed to get this working.

I've compared my implementation to the MS documentation and other answers around SO and none of the approaches seem to work.

The root of the problem is the switch from an anonymous user and a logged in user.

I've been following the MS advice here. And various answers here and here

For testing I have one contact form with an endpoint decorated with [ValidateAntiForgeryToken].

The flow is:

Visit the site, post this form and everything works fine. Login Visit the form, post - BOOM - The provided antiforgery token was meant for a different claims-based user than the current user.

In my public void Configure( method, I have:

app.Use(async (context, next) =>
{
        var tokens = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false });

    await next();
});

In my public void ConfigureServices( method I have:

services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");

In my Vue Router I have added a call to a method on my axios API like this:

router.afterEach((to, from) => {
    api.readCsrfCookieAndSetHeader();
});

This method just reads the cookie and updates the header:

public readCsrfCookieAndSetHeader() {
    console.info('READING CSRF-TOKEN');
    if (document.cookie.indexOf('CSRF-TOKEN') > -1) {
        const v = document.cookie.match('(^|;) ?' + 'CSRF-TOKEN' + '=([^;]*)(;|$)');
        const r = v ? v[2] : '';
        // console.log(r);
        this.csrfToken = r;
        axios.defaults.headers.common['X-CSRF-TOKEN'] = this.csrfToken;
        console.log(axios.defaults.headers.common['X-CSRF-TOKEN']);
    } else {
        this.csrfToken = '';
    }
}

I can see this value changing page by page. One suggestion that seem to have worked for some is to rerun GetAndStoreTokens at the point where the user is signed in, such as:

var user = await _userManager.FindByEmailAsync(userName);
var result = await _signInManager.PasswordSignInAsync(user, password, true, false);
_httpContextAccessor.HttpContext.User = await _signInManager.CreateUserPrincipalAsync(user);
if (result.Succeeded)
{
    // get, store and send the anti forgery token
    AntiforgeryTokenSet tokens = _antiforgery.GetAndStoreTokens(_httpContextAccessor.HttpContext);
    _httpContextAccessor.HttpContext.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false });
}

return result;

But this hasn't worked for me either.

I've also tried updating the value with an axios interceptor, like this:

axios.interceptors.response.use(
    (response) => {
        // this.readCsrfCookieAndSetHeader();
        return response;
    }, 
    (error) => {

But that's really just really another way in to get the value updated that I'm sure is already being updated.

I've run out of ideas and, it seems, things to try. Hence this Q.

Have I missed anything obvious? It seems like I've replicated the MS Angular example nearly verbatim so I'm at a loss as to what I've done wrong.

Any pointers would be very much appreciated.


Solution

  • As discussed in the comments on your question. I have a feint memory of it being related to the ordering of something in the AppStartup. Here is a dump of what I have. This currently works (well seems to).

        /// <summary>
        /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        /// </summary>
        /// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
        /// <param name="env">The <see cref="IHostingEnvironment"/>.</param>
        /// <param name="antiforgery">Enables setting of the antiforgery token to be served to the user.</param>
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAntiforgery antiforgery)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
                {
                    HotModuleReplacement = true,
                });
            }
    
            app.UseSession();
    
            app.UseHttpsRedirection();
    
            app.UseStaticFiles();
    
            // global cors policy
            app.UseCors(x => x
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader());
    
            // Authenticate before the user accesses secure resources.
            app.UseAuthentication();
    
            app.Use(next => context =>
            {
                string path = context.Request.Path.Value;
                if (path.IndexOf("a", StringComparison.OrdinalIgnoreCase) != -1 || path.IndexOf("b", StringComparison.OrdinalIgnoreCase) != -1)
                {
                    // The request token can be sent as a JavaScript-readable cookie,
                    // and Angular uses it by default.
                    var tokens = antiforgery.GetAndStoreTokens(context);
                    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
                }
    
                return next(context);
            });
    
            app.Use(next => context =>
            {
                string timezone = context.Request.Headers["Timezone"];
    
                if (!string.IsNullOrEmpty(timezone))
                {
                    context.Session.SetString(nameof(HttpContextSessionValues.SessionStrings.Timezone), timezone);
                }
    
                return next(context);
            });
    
            app.UseExceptionHandler(errorApp =>
            {
                errorApp.Run(async context =>
                {
                    context.Response.StatusCode = 500;
                    context.Response.ContentType = "text/html";
    
                    var exHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
                    var exception = exHandlerFeature.Error;
    
                    if (exception is PresentableException)
                    {
                        await context.Response.WriteAsync(exception.Message).ConfigureAwait(false);
                    }
                    else
                    {
                        await context.Response.WriteAsync("An Unexpected error has occured. You may need to try again.").ConfigureAwait(false);
                    }
                });
            });
            app.UseHsts();
    
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
    
                routes.MapSpaFallbackRoute(
                    name: "spa-fallback",
                    defaults: new { controller = "Home", action = "Index" });
            });
        }