Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asp.net core 2 + Antiforgery: all POST endpoints return 400 Bad Request #3027

Closed
Corstiaan84 opened this issue Apr 5, 2018 · 13 comments
Closed
Assignees
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates investigate

Comments

@Corstiaan84
Copy link

Corstiaan84 commented Apr 5, 2018

I have an asp.net core 2.1.4 app with a custom IXmlRepository to store the app keys using ef core + postgres. I deploy the app using Docker.

Everything deploys and run fine except that all POST request return a 400 bad request response when running in Production. All is fine in Development.

The problem is related to the setup of the antiforgery/dataprotection settings, I think

This is my Startup.cs when I config the DataProtection and Antiforgery stuff. I have redacted it minimal just in case some other settings might be interfering.

public class Startup
{
    IHostingEnvironment _env;

    public Startup(IConfiguration configuration, IHostingEnvironment env)
    {
        Configuration = configuration;
        _env = env;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<AppDbContext>(options =>
            options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"))
        );

        services.AddIdentity<User, Role>()
        .AddEntityFrameworkStores<AppDbContext>()
        .AddDefaultTokenProviders();

        services.Configure<IdentityOptions>(options =>
        {
            // Password settings
            options.Password.RequireDigit = true;
            options.Password.RequiredLength = 8;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireUppercase = true;
            options.Password.RequireLowercase = false;
            options.Password.RequiredUniqueChars = 6;

            // Lockout settings
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
            options.Lockout.MaxFailedAccessAttempts = 10;
            options.Lockout.AllowedForNewUsers = true;

            // User settings
            options.User.RequireUniqueEmail = true;
        });

        ### HERE I SET THE CUSTOM KEY STORE ###

        services.AddSingleton<IXmlRepository, EfXmlRepository>();
        var sp = services.BuildServiceProvider();
        services.AddDataProtection()
                .AddKeyManagementOptions(options => options.XmlRepository = sp.GetService<IXmlRepository>());

        services.AddMvc(options =>
        {
            options.OutputFormatters.Add(new HtmlOutputFormatter());
            options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
        });

        ### HERE I SET THE ANTI FORGERY HEADER FOR SOME API CALLS ###

        services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN");

        services.AddAuthentication()
        .AddCookie()         
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = "myapp.com",
                ValidAudience = "myapp.com",
                RequireExpirationTime = false,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["SecurityKey"]))
            };
        });;

        services.AddTransient<JwtService>();
        services.AddTransient<PaymentProviderService>();
        services.AddTransient<SubscriptionService>();
        services.AddTransient<IInvalidJwtTokenStore, EfInvalidJwtTokenStore>();

        //setup hangfire
        GlobalConfiguration.Configuration.UsePostgreSqlStorage(Configuration.GetConnectionString("DefaultConnection"));

    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();
        app.UseAuthentication();

        app.UseMvc(
            routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            }
        );

        loggerFactory.AddConsole(LogLevel.Information);
    }

This is my ef core IXmlRepository implementation:

public class EfXmlRepository : IXmlRepository
{
    AppDbContext _db;

    public EfXmlRepository(AppDbContext dbContext)
    {
        _db = dbContext;
    }

    public IReadOnlyCollection<XElement> GetAllElements()
    {
        var list = _db.XmlKeys.ToList().Select(x => XElement.Parse(x.Xml)).ToList();
        return list;
    }

    public void StoreElement(XElement element, string friendlyName)
    {
        _db.XmlKeys.Add(new XmlKey
        {
            Xml = element.ToString(SaveOptions.DisableFormatting)
        });

        _db.SaveChanges();
    }
}

At an earlier stage the logs told me this:

2018-04-04T18:05:58.950344099Z app[web.1]: fail: 
Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery[7]
2018-04-04T18:05:58.950392982Z app[web.1]:       An exception was 
thrown while deserializing the token.
2018-04-04T18:05:58.950398037Z app[web.1]: 
System.InvalidOperationException: The antiforgery token could not be 
decrypted. ---> System.Security.Cryptography.CryptographicException: 
The key {9b8ab2d0-3ca5-4fcd-af54-b1a6601077af} was not found in the key 
ring.

But this error disappeared when I tweaked the postgres settings for the IXmlRepository.

Just in case this is my Dockerfile

FROM microsoft/dotnet:latest
COPY . /app
WORKDIR /app

ENV ASPNETCORE_URLS http://0.0.0.0:5000
ENV ASPNETCORE_ENVIRONMENT Production
EXPOSE 5000

RUN dotnet restore
RUN dotnet publish -c Release

WORKDIR /app/MyApp.Web
RUN dotnet ef database update

WORKDIR /app/MyApp.Web/bin/Release/netcoreapp2.0/publish
ENTRYPOINT dotnet MyApp.Web.dll

Looking forward ot hearing your thoughts. Thanks!

@natemcmaster
Copy link
Contributor

But this error disappeared when I tweaked the postgres settings for the IXmlRepository

From this sentence, it sounds like the antiforgery error message isn't appearing any more. Can you clarify what problem you are having now?

@Corstiaan84
Copy link
Author

All POST endpoints in my app return 400 Bad request. I have an api endpoint that expects json and an another endpoint that expects a regular model POSTed by a form. Both keep returning 400.

I just created an empty app (File -> New) with and POST endpoint to test my production env without all the config of my current app, just to see if my production env has some quirks. Used the same Dockerfile and deployed the dummy app. The POST endpoint works fine so it's not the env. So somewhere my config is wrong I guess.

Regarding the antiforgery error message, it's not appearing any more but I still have the feeling something is wrong there. I set loggerFactory.AddConsole(LogLevel.Information); to loggerFactory.AddConsole(LogLevel.Trace);, but all I see in the logs is:

2018-04-05T17:00:58.539262786Z app[web.1]: Hosting environment: Production
2018-04-05T17:00:58.539410053Z app[web.1]: Content root path: /app/MyApp.Web/bin/Release/netcoreapp2.0/publish
2018-04-05T17:00:58.539518451Z app[web.1]: Now listening on: http://0.0.0.0:5001
2018-04-05T17:00:58.539561707Z app[web.1]: Application started. Press Ctrl+C to shut down.

Also when hitting the POST endpoints and creating the 400's. This also doesnt seem right, correct? Given I have set the loglevel to Trace?

(Almost dinner time here in the Netherlands. Will reply later tonight...)

@Corstiaan84
Copy link
Author

@natemcmaster Can I provide additional information to clarify?

@Corstiaan84
Copy link
Author

I also tried persisting the key to the filesystem to a mounted volume on the docker host. However, I am running into this issue.

@natemcmaster
Copy link
Contributor

Thanks for clarifying. @mkArtakMSFT or @rynowak - looks like antiforgery issue. Could be a misconfiguration of the app or browser, but I'm not sure as antiforgery is not my forte.

Re: #2941 - if you have repro steps, please feel free to comment on that issue. We closed it because we don't have enough info to investigate, and it appeared to be an issue in .NET Core with System.IO on certain hardware.

@jhudsoncedaron
Copy link

I think this bug is real; I've seen too many bugs in kestrel where I/O errors get reported as other nonsense rather than bubbling up as the I/O errors they are and turning into 5xx HTTP error codes.

@Corstiaan84
Copy link
Author

Corstiaan84 commented Apr 7, 2018

@natemcmaster @mkArtakMSFT @rynowak Thank you for looking into this. If this is a proper bug I am eager to provide more info in order to solve it as my project is halted due to this. Let me know what you need.

@Corstiaan84
Copy link
Author

For anyone else stumbling onto this issue, this was the root of the problem.

@MrComic
Copy link

MrComic commented Sep 12, 2018

I have the same problem
https://stackoverflow.com/questions/52290736/xsrf-token-conflicts-with-jwt-token

@mkArtakMSFT
Copy link
Member

Thanks for contacting us, @Corstiaan84.
@javiercn, can you please look into this? Thanks!

@javiercn
Copy link
Member

@MrComic Your issue is different than the one expressed by @Corstiaan84.
Here are some general recommendations for your case:

Regarding mixing pages and API endpoints on the same application:
This usually means that you are either using cookie authentication for your APIs or that you have separate authentication mechanisms for your API endpoints and your webpage endpoints. The recommendation here is:

  1. Do not use cookies to protect API endpoints. Use something more appropriate for APIs like JSON Web Tokens.
  2. Separate the different parts of your application based on the authentication mechanism being used. For example:
    • Put all your APIs under /api and use JWTs for authentication.
    • Put all your pages under /site and use Cookies for authentication.
  3. Unless your APIs accept one of the content-types described above, disable anti-forgery from your API endpoints.
  4. If you require an endpoint that needs to accept any of the content-types described above in addition to JSON. Separate the endpoint into two actions using ConsumesAttribute and enable antiforgery only on the endpoint that needs to accept the content-types "application/x-www-form-urlencoded", "multipart/form-data", "text/plain".

@javiercn
Copy link
Member

javiercn commented Oct 4, 2018

Closing this as there's no more action to be taken here.

@javiercn javiercn closed this as completed Oct 4, 2018
@Eilon Eilon added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates and removed repo:Antiforgery labels Nov 7, 2018
@afernandes
Copy link

afernandes commented Dec 26, 2018

https://stackoverflow.com/questions/50064246/asp-net-core-razor-ajax-post-400-bad-request

This link helped me solve the same problem

Attention to: services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN");

beforeSend: function (xhr) {
      xhr.setRequestHeader("X-XSRF-Token",
            $('input:hidden[name="__RequestVerificationToken"]').val());
      },

@ghost ghost locked as resolved and limited conversation to collaborators Dec 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates investigate
Projects
None yet
Development

No branches or pull requests

8 participants