Search code examples
c#authenticationiisauthorization.net-6.0

ASP.NET Core (React) app - 404.15 on the IIS


I've found many threads about this issue, but not matter what I change it just doesn't work. I am desperate as I don't understand what is wrong.

My application works fine if run on IIS Express.

As soon as I deploy it to the IIS, it starts to complain about the "query string is too long"

What I don't understand is why it is calling Account\Login - my application has no controllers or any views. It is ASP.NET Core React template where we don't use controllers but our communication is fully through SignalR. We don't have even API controllers.

I tried to set web.config as is suggested on many places (for example here but it's suggested in many other threads) - it doesn't help.

I have identified that the issue in the code is setup of authorization in Program.cs/Startup .cs file. However, it is done according to Microsoft guide.

In my Program.cs I have this to configure authentication:

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
builder.Services.AddAuthorization(options => options.FallbackPolicy = options.DefaultPolicy);

If I run application via the IIS Express, it loads fine, authenticates user and everything seems to be working just fine. As soon as I deploy it to the IIS server, I receive this error: enter image description here

Note: I have noticed that even if run on IIS Express, it still sends requests to /Account/Login but it doesn't crash the app for some reason.

What I really don't understand is, why is it calling Account/Login path, it just doesn't exist and I don't have it set anywhere in the code. And as mentioned, my application have no controllers or views. It's react application communicating with server via SignalR (but it doesn't get that far, so SignalR is not involved yet).

If I change the code above to:

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
builder.Services.AddAuthorization(options => options.FallbackPolicy = null);

then it won't crash, but it won't log in windows user either.

I tried many things in last two days but can't figure out, what is wrong.

I appreciate any ideas of what could be wrong.

Edit 1 - web.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
    <security>
        <requestFiltering removeServerHeader="true">
            <requestLimits maxUrl="4096" maxQueryString="5000"></requestLimits>
        </requestFiltering>
    </security>
<httpProtocol>
    <customHeaders>
        <remove name="X-Powered-By" />
        <add name="X-Frame-Options" value="DENY" />
    </customHeaders>
</httpProtocol>
<handlers>
    <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="bin\x86\Debug\net6.0-windows\PPSim.Web.exe" arguments="" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" startupTimeLimit="3600" requestTimeout="23:00:00" hostingModel="inprocess">
  <environmentVariables>
    <environmentVariable name="ASPNETCORE_HTTPS_PORT" value="443" />
    <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
    </environmentVariables>
</aspNetCore>
</system.webServer>
<system.web>
    <httpRuntime enableVersionHeader="false" maxQueryStringLength="32768" maxUrlLength="65536" />
</system.web>
</configuration>

Edit 2 - Program.cs:

var builder = WebApplication.CreateBuilder(args);

#region Services

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
builder.Services.AddAuthorization(options => options.FallbackPolicy = options.DefaultPolicy);

builder.Services.AddAntiforgery(options =>
{
    options.HeaderName = CsrfConstants.CsrfHeaderName;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict;
});

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

var settings = new JsonSerializerSettings { ContractResolver = new SignalRContractResolver() };
var serializer = JsonSerializer.Create(settings);
builder.Services.AddSingleton(serializer);

builder.Services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
});

builder.Services.AddSingleton<IUserSettingsWrapper, UserSettingsWrapper>();
builder.Services.AddSingleton<ISignalREventsPusher, SignalRDataPusher>();


builder.Services.AddSignalR().AddNewtonsoftJsonProtocol(options =>
{
    options.PayloadSerializerSettings.ContractResolver = new CustomCamelCasePropertyNamesContractResolver();
    options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
    options.PayloadSerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
});

builder.Services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
builder.Services.AddLogging(loggingBuilder =>
{            loggingBuilder.AddConfiguration(builder.Configuration.GetSection("Logging"));
    loggingBuilder.AddConsole();
    loggingBuilder.AddDebug();
});

builder.Services.AddCors(options =>
    {
        options.AddPolicy("CORSPermission", policy =>
        {
            policy.AllowAnyHeader()
                .AllowAnyMethod()
                .SetIsOriginAllowed(host => true)
                .AllowCredentials();
        });
    });

#endregion

var app = builder.Build();

#region Configure

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(CsrfConstants.CsrfCookiesName, tokenSet.RequestToken!,
            new CookieOptions
            {
                HttpOnly = false,
                Secure = true, // 15018: Cookie generated by application does not contain the “Secure flag” attribute
                SameSite = SameSiteMode.Strict
            });
    }

    return next(context);
});

if (!app.Environment.IsDevelopment())
{
}
else
{
    app.UseHsts();
    //app.UseExceptionHandler("/Home/Error");
}

app.UseStaticFiles();

app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("CORSPermission")

app.UseAuthentication();
app.UseAuthorization();
app.UseFileServer();

#endregion

app.MapFallbackToFile("index.html");

app.UseEndpoints(endpoints =>
{
    endpoints.MapHub<SimulatorHub>("/SimulatorHub",
        options => options.Transports = GetTransportToUse(app.Environment)).RequireCors("CORSPermission");
});

app.Run();

Solution

  • As a follow-up on our insights from the comments section in your question.

    There's indeed an infinite redirect going on to the account/login url.

    Since you don't have such a controller and/or page, that one is provided out-of-the-box by ASP.NET Core Identity.

    From that page.

    ASP.NET Core Identity adds user interface (UI) login functionality to ASP.NET Core web apps.

    The generated project provides ASP.NET Core Identity as a Razor Class Library.
    The Identity Razor Class Library exposes endpoints with the Identity area. For example:

    • /Identity/Account/Login

    That does get activated by below call you have in program.cs.

    builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
    

    To resolve the issue, that line is to be removed since you'll be using Windows authentication.