dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.19k stars 9.93k forks source link

Bad Request - Request Too Long - IIS Server #57545

Open kuldeepcis-lab opened 2 weeks ago

kuldeepcis-lab commented 2 weeks ago

Is there an existing issue for this?

Describe the bug

I cloned the repository and started working with the BlazorWebappOidc project. I configured the Entra ID settings, such as the client ID and tenant ID, using the details from my Entra ID tenant account. After running the application locally on the Kestrel server with the HTTPS profile, the Blazor application launched successfully. The interface loaded in the browser, and I was able to log in using my Entra ID account. After a successful callback to my Blazor application URL, I was redirected and received the expected details (Such as Username, email address, etc).

However, when I deployed the same application on an IIS server, I encountered an issue. While the Blazor application opened, when I attempted to log in to my Microsoft Entra ID account, I received a Bad Request - Request Too Long (HTTP Error 400. The size of the request headers is too long) error after the callback.

Expected Behavior

After a successful callback to my Blazor application URL, I was redirected and received the expected details (Such as Username, email address, etc).

Steps To Reproduce

"iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:46294", "sslPort": 44381 } },"profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } }

Bad Request - Request Too Long (HTTP Error 400. The size of the request headers is too long.)

Exceptions (if any)

No response

.NET Version

DOTNET 8

Anything else?

Screenshot_10

cc: @guardrex https://github.com/dotnet/blazor-samples/issues/344

martincostello commented 2 weeks ago

Try configuring IIS Request Limits in web.config like this to increase the allowed query string length:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <!-- Any existing configuration you may have -->
+ <system.webServer>
+   <security>
+     <requestFiltering>
+       <requestLimits maxQueryString="4096" />
+     </requestFiltering>
+   </security>
+ </system.webServer>
</configuration>
kuldeepcis-lab commented 2 weeks ago

Already tried, but Not Worked.

javiercn commented 2 weeks ago

@kuldeepcis-lab thanks for contacting us.

The request that fails is the one that happens after the logging, isn't it? Seems that too much information might be stored in the auth cookie. (There are 5 chunks at 4Kb per chunk)

So I suspect IIS has a 16KB header limit.

In that situation, you need to either configure a higher limit (not sure if possible) or reduce the amount of information that you put on the cookie.

You can do so by hooking on to the Oidc call and customizing the claims principal https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims?view=aspnetcore-8.0

utkarshdubeyfsd commented 2 weeks ago

@kuldeepcis-lab, Try configuring the other IIS limits in the web.config file, such as increasing the request length and content length.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <!-- Increase max request length in KB -->
    <httpRuntime maxRequestLength="51200" /> <!-- 50 MB -->
  </system.web>

  <system.webServer>
    <security>
      <requestFiltering>
        <!-- Increase max content length in bytes -->
        <requestLimits maxAllowedContentLength="52428800" /> <!-- 50 MB -->
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>
kuldeepcis-lab commented 2 weeks ago

Hello @utkarshdubeyfsd I had already tried both of the solutions but did not work at all.

kuldeepcis-lab commented 2 weeks ago

Hello @javiercn

I had tried setting the header limit to 64 KB, but no luck even after that change.

guardrex commented 2 weeks ago

@kuldeepcis-lab ... You tried by setting the registry keys?

kuldeepcis-lab commented 2 weeks ago

@guardrex No, I do not know, Can you please guide me.

guardrex commented 2 weeks ago

Note that I'm not recommending this even if it will work. The product unit must state what approach should be taken here. We will need to do something in the article/sample to address this because this is a failure with the BWA+OIDC sample app OOB with no special config.

See ...

https://learn.microsoft.com/troubleshoot/developer/webapps/iis/iisadmin-service-inetinfo/httpsys-registry-windows

... for MaxFieldLength and MaxRequestBytes.

Again ... I mention this ⚠️ for testing here to get to the root problem. ⚠️

See if that even lets the app run properly, and then @javiercn / @halter73 can discuss with us how the article/sample app should really address this. I kind'a doubt that we'll be including guidance on how to adjust registry keys to let the OOB app run under IIS.

I've opened a docs issue 👇 for when there's guidance to publish.

kuldeepcis-lab commented 2 weeks ago

@guardrex by adding the MaxFieldLength and MaxRequestBytes parameter to registry. it is still not working

guardrex commented 2 weeks ago

Ok ... thanks for checking (and I assume that you changed those and restarted the server).

I'm 👂 here for the discussion/resolution, and I'll get the article+sample updated per the docs issue that I opened as soon as I know what to do.

halter73 commented 2 weeks ago

@kuldeepcis-lab What did you try setting MaxRequestBytes to? Did you confirm changing this setting allowed you to manually send a request with more than 16 KiB of request headers?

Even Kestrel has a limit on the total size of request headers. If Kestrel can handle request headers of this size without custom configuration, IIS should also be able to with the right configuration.

The default MaxRequestHeadersTotalSize for Kestrel is 32 KiB while IIS's MaxRequestBytes (which is nearly equivalent except that the IIS limit also counts the size of the request line in addition to the headers) is 16 KiB, but this can be increased all the way to 16 MiB.

There's also the MaxFieldLength, but that shouldn't come into play considering the cookies are chunked at 4 KiB per header, and the MaxFieldLength limit is 16 KiB per header.

However, I think @javiercn is on the right track. You probably don't want to take the performance hit of sending such large cookies every request anyway. Your best bet is likely to first inspect which claims are getting stored in the cookie and try clearing any unnecessary claims from the cookie using OpenIdConnectOptions.ClaimActions.DeleteClaim. https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/additional-claims?view=aspnetcore-8.0#remove-claim-actions-and-claims

https://learn.microsoft.com/troubleshoot/developer/webapps/iis/www-administration-management/http-bad-request-response-kerberos has some similar guidance to https://learn.microsoft.com/troubleshoot/developer/webapps/iis/iisadmin-service-inetinfo/httpsys-registry-windows, but it's first suggestion is to "Decrease the number of Active Directory groups". This is for Kerberos rather than OIDC, but the principle is the same. If you try to encode too many claims into a header (in this case the "Cookie" header), you'll eventually run into limits.

kuldeepcis-lab commented 2 weeks ago

Hi @halter73 BlazorWebappOidc Works locally when launched using Visual Studio and it runs on Kestral Server on Https profile by default and we do not need to customize the headers but having issues With the IIS Server, Not only on deployment The issue is persistent when run locally on IIS through Visual Studio

Now trying to Delete the unnecessary claim OpenIdConnectOptions.ClaimActions.DeleteClaim will back soon.

kuldeepcis-lab commented 2 weeks ago

Hi @halter73 Reducing the claim size helps but also creates an issue that the application does not contain the Authentication state I think because whenever I tried to switch between the pages the application automatically goes down.

guardrex commented 2 weeks ago

Given that this issue has arisen and concern that other devs are going to face it in the future with users who have many AD groups/AD built-in Admin Roles, I now plan to modify the approach described by the article.

Let's obtain AD groups/AD built-in Admin Roles via Graph SDK/API separately for authenticating users on sign in. It will eliminate this problem for everyone forever. I already have a separate issue to include an add-on section for Graph SDK/API setup anyway.

@kuldeepcis-lab ... The soonest that I can reach it is next week (if no fires come up between now and then) due to a couple of factors ... 🦖🧠🔥😆 ... I'm tired and need a few days off. We also have the Labor Day holiday on Monday. If you can wait until next week, I'll have a new Graph-based approach for you.

@halter73 @MackinnonBuck @javiercn ... If everyone is in agreement with my plan, this PU issue can be closed in favor of the existing docs issues ...

@halter73 ... I'll ping u for review when the docs PR goes up.

guardrex commented 1 week ago

@kuldeepcis-lab ... I have a working 🙈 RexHacks!™ 🙈 approach that uses Graph SDK/API now. It doesn't rely upon sending AD groups and AD built-in Admin Roles via the cookie, so it should work in your scenario. However, a few caveats ...

The simplest way for me to show you my 🦖 hacks is to toss this prototype app into GH for you to look at. You can adopt its approaches ⚠️ AT YOUR OWN RISK ⚠️😨 until an official sample+coverage is released. The README has the particulars. If you want to discuss the sample, open an issue on that sample repo. This isn't an official sample, and we won't discuss it here, unless Halter/Javier/Mackinnon want to chat about it.

https://github.com/guardrex/BlazorWebAppMSIdentityWeb/

@halter73 may or may not look at that. Idk if he has time for such silly 🦖 things! 😆

guardrex commented 1 week ago

I took one more shot with the Graph client + BWA+OIDC, and it still gives me a 💥. It complains about a malformed request using the OBO credential provider.

I'll throw a dog a bone tho ... REST API to the rescue! 🚑🚒🚓.

It's not "nice" in the SDK sense, but it works ✨. It has the nice benefit of requiring no packages, so it's very lean.

In the CookieOidcRefresher.cs, I made a nullable string property for the access token (CurrentAccessToken) and assign to it at the top and the bottom of the ValidateOrRefreshCookieAsync method. That keeps it updated regardless of refreshing the cookie.

Need a couple of classes for deserializing ...

public class RootObject
{
    [JsonPropertyName("odatacontext")]
    public string? Context { get; set; }

    [JsonPropertyName("value")]
    public Value[]? Value { get; set; }
}

public class Value
{
    [JsonPropertyName("@odata.type")]
    public string? Type { get; set; }

    [JsonPropertyName("id")]
    public string? Id { get; set; }

    [JsonPropertyName("roleTemplateId")]
    public string? RoleTemplateId { get; set; }
}

In PersistingAuthenticationStateProvider.cs, inject the CookieOidcRefresher as cookieOidcRefresher.

OnPersistingAsync looks like this ...

private async Task OnPersistingAsync()
{
    var authenticationState = await GetAuthenticationStateAsync();
    var principal = authenticationState.User;

    if (principal.Identity?.IsAuthenticated == true)
    {
        var claimsIdentity = (ClaimsIdentity)principal.Identity;
        var accessToken = cookieOidcRefresher.CurrentAccessToken;

        var client = new HttpClient();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        var response = await client.GetAsync("https://graph.microsoft.com/v1.0/me/memberOf");

        if (response.StatusCode == HttpStatusCode.OK)
        {
            var content = await response.Content.ReadAsStringAsync();
            RootObject? rootObject = JsonSerializer.Deserialize<RootObject>(content);

            var groupsAndRoles = rootObject?.Value;

            if (groupsAndRoles is not null)
            {
                foreach (var entry in groupsAndRoles)
                {
                    if (entry.Type == "#microsoft.graph.group" && entry.Id is not null)
                    {
                        claimsIdentity.AddClaim(new Claim("groups", entry.Id));
                    }

                    if (entry.Type == "#microsoft.graph.directoryRole" && entry.RoleTemplateId is not null)
                    {
                        claimsIdentity.AddClaim(
                            new Claim("directoryRole", entry.RoleTemplateId));
                    }
                }
            }
        }
        else
        {
            throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}.");
        }

        principal = new ClaimsPrincipal(claimsIdentity);
        persistentComponentState.PersistAsJson(nameof(UserInfo), UserInfo.FromClaimsPrincipal(principal));
    }
}

This won't be for docs. I'm just placing this here in passing in case anyone wants to see how I did it in the BWA+OIDC app. If anyone figures out the Graph client approach without 'malformed requests', I'd like to hear about it 👂. I still think we're going to move the AD groups/roles section into a new article on BWA+MS Identity Web, where I know the API call to Graph works well for AD groups/roles 🎉. MS Identity Web IS the Entra-recommended way to go.

@halter73 ... My hacked sample for BWA+MS Identity Web is linked in the prior comment, but I'll wait for an official sample before writing anything up.

halter73 commented 1 week ago

At a glance, https://github.com/guardrex/BlazorWebAppMSIdentityWeb/ looks pretty good to me. You might be able to leverage AddMicrosoftIdentityUI to get rid of LoginLogoutEndpointRouteBuilderExtensions, but it's nice that the latter doesn't rely on MVC, so I understand leaving it there. I might do the same in the "official" version we use for the template.

Instead of putting the Microsoft Graph logic inside of OnPersistingAsync, you could do the same thing in OpenIdConnectOptions.Events.OnTokenValidated. It might also be worth checking if you can get the same claims you can from Microsoft Graph if you configure OpenIdConnectOptions.GetClaimsFromUserInfoEndpoint = true. If we did that, we'd need to update CookieOidcRefresher to also query the userinfo_endpoint. I agree that using AddMicrosoftIdentityWebApp is best if you're connecting to Entra ID. And I'm not sure how much the data userinfo_endpoint overlaps with the Microsoft Graph data.

I'm also not sure that JwtSecurityTokenHandler.DefaultMapInboundClaims = false does anything anymore, since JwtSecurityToken has been deprecated and is no longer used unless you configure UseSecurityTokenValidators = true. https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events

You should be able to achieve the same result with JsonWebToken by calling JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear(). Or if you don't want to rely on mutating static state, you can still configure OpenIdConnectOptions.MapInboundClaims = false as follows:

builder.Services.Configure<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme, oidcOptions =>
{
    oidcOptions.MapInboundClaims = false;
});

It's also probably better to use an array of DownstreamApi scopes rather than splitting a single string with builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' '). AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi")) should already be reading the scopes as an array. I'm not sure if EnableTokenAcquisitionToCallDownstreamApi needs the initial scopes, but if it does, you should be able to manually read them as an array without splitting using builder.Configuration.GetValue<string[]>("DownstreamApi:Scopes").

Also, where is HandleSameSiteCookieCompatibility() coming from? And what does it do exactly?

guardrex commented 1 week ago

For ...

I'm also not sure that JwtSecurityTokenHandler.DefaultMapInboundClaims = false does anything anymore

... and ...

HandleSameSiteCookieCompatibility() coming from?

Those are taken from the Azure docs sample app. It might be a Jean-Marc sample. ~I don't have the link handy at the moment, but I'll post it tomorrow.~ One of my source links is lost. No matter. I'm going to update per your tips :point_up:.

builder.Configuration.GetValue("DownstreamApi:Scopes")?.Split(' ')

I think that came from the sample, too.

For the rest, I'll see if I can take those approaches with the sample app to improve it. I'll let you know how it turns out.

guardrex commented 1 week ago

I found ...

https://github.com/microsoftgraph/msgraph-sample-aspnet-core/tree/main/GraphTutorial

... and based a set of updates on that. They're using extension methods and grabbing a lot more Graph data. I'm just hacking a minimal approach without all the bells and whistles at this point.

Here are my latest 🦖 Hack'ins!™ ...

https://github.com/guardrex/BlazorWebAppMSIdentityWeb

When we reach the point of having an official BWA+MS Identity Web sample that I can write a new article from, I'll include steps in an article section. I'll probably mirror the approach that the Azure sample takes with one or more fancy extension methods. I'll also add some Key Vault code via Managed Identity for the client secret when writing it up, and I'll back-port the new Key Vault coverage to the BWA+OIDC article.