Search code examples
c#angularasp.net-coreangular13asp.net-core-6.0

How to properly implement Windows Authentication in an ASP.NET Core app with Angular


I just finished creating an ASP.NET Core app with Angular as described in this tutorial. When creating the ASP.NET Core project, I checked the option to enable Windows Authentication. As long as I add [AllowAnonymous] to my controllers in the ASP.NET Core project, everything already works just fine, but as soon as I replace that with [Authorize], the requests from my Angular app no longer reach my code in the backend. Instead, the webserver returns a 400 Bad Request error. Here is a request including the response headers where this problem occurs: Example 400 Bad Request

After doing some research on my own, I found out that I probably need to enable CORS in the backend, since I am using a proxy via proxy.conf.json in the Angular app during development. Here is the content of the proxy.conf.json:

{
  "/api/*": {
    "target": "https://localhost:7139",
    "secure": false,
    "changeOrigin": true
  }
}

Therefore, I tried to add UseCors() in my Program.cs of the ASP.NET Core project. Here is how the Program.cs currently looks like:

using Microsoft.AspNetCore.Authentication.Negotiate;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddCors(setup =>
{
    setup.AddDefaultPolicy(builder =>
    {
        builder.WithOrigins("http://localhost:4200")
          .AllowAnyHeader()
          .AllowAnyMethod()
          .AllowCredentials();
    });
});

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();

builder.Services.AddAuthorization(options =>
{
    // By default, all incoming requests will be authorized according to the default policy.
    options.FallbackPolicy = options.DefaultPolicy;
});

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseCors();

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

app.MapControllers();

app.Run();

Unfortunately, this does not seem to make any difference. This is how my http GET call is implemented in the frontend:

this.http.get<string>(`api/test/1`, { responseType: 'text', withCredentials: true } as Record<string, unknown>).subscribe(value => {
      alert(value);
    }, error => console.error(error));

I feel like I am missing something important which I need to make Windows Authentication work here, but I don't know what exactly I am doing wrong. I do not even get any additional information from the webserver about why it even returns 400 Bad Request here, so I don't know where to start debugging this issue.

UPDATE: I figured out how to enable more Kestrel-Logging by adding the following configuration-entries in the Logging section of my appsettings.Development.json:

"Default": "Debug",
"Microsoft.AspNetCore": "Debug",
"Microsoft.AspNetCore.Server.Kestrel": "Debug",
"Microsoft.AspNetCore.Server.Kestrel.BadRequests": "Debug",
"Microsoft.AspNetCore.Server.Kestrel.Connections": "Debug",
"Microsoft.AspNetCore.Server.Kestrel.Http2": "Debug",
"Microsoft.AspNetCore.Server.Kestrel.Http3": "Debug"

Here is what Kestrel is logging while handling the request which ends with a 400 Bad Request error:

dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[39]
      Connection id "0HMGSHHNJQ68N" accepted.
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[1]
      Connection id "0HMGSHHNJQ68N" started.
dbug: Microsoft.AspNetCore.Server.Kestrel.Https.Internal.HttpsConnectionMiddleware[3]
      Connection 0HMGSHHNJQ68N established using the following protocol: Tls12
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET https://localhost:7139/api/test/1 - -
dbug: Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware[0]
      Wildcard detected, all requests with hosts will be allowed.
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1001]
      1 candidate(s) found for the request path '/api/test/1'
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1005]
      Endpoint 'WebApp.Controllers.TestController.Get (WebApp)' with route pattern 'api/Test/{id}' is valid for the request path '/api/test/1'
dbug: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[1]
      Request matched endpoint 'WebApp.Controllers.TestController.Get (WebApp)'
dbug: Microsoft.AspNetCore.Authentication.Negotiate.NegotiateHandler[9]
      AuthenticationScheme: Negotiate was not authenticated.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
      Authorization failed. These requirements were not met:
      DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
dbug: Microsoft.AspNetCore.Authentication.Negotiate.NegotiateHandler[6]
      Challenged 401 Negotiate.
info: Microsoft.AspNetCore.Authentication.Negotiate.NegotiateHandler[12]
      AuthenticationScheme: Negotiate was challenged.
dbug: Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[6]
      Connection id "0HMGSHHNJQ68N" received FIN.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/1.1 GET https://localhost:7139/api/test/1 - - - 401 0 - 189.6680ms
dbug: Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[7]
      Connection id "0HMGSHHNJQ68N" sending FIN because: "The client closed the connection."
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[10]
      Connection id "0HMGSHHNJQ68N" disconnecting.
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[2]
      Connection id "0HMGSHHNJQ68N" stopped.
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[39]
      Connection id "0HMGSHHNJQ68O" accepted.
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[1]
      Connection id "0HMGSHHNJQ68O" started.
dbug: Microsoft.AspNetCore.Server.Kestrel.Https.Internal.HttpsConnectionMiddleware[3]
      Connection 0HMGSHHNJQ68O established using the following protocol: Tls12
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET https://localhost:7139/api/test/1 - -
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1001]
      1 candidate(s) found for the request path '/api/test/1'
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1005]
      Endpoint 'WebApp.Controllers.TestController.Get (WebApp)' with route pattern 'api/Test/{id}' is valid for the request path '/api/test/1'
dbug: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[1]
      Request matched endpoint 'WebApp.Controllers.TestController.Get (WebApp)'
info: Microsoft.AspNetCore.Authentication.Negotiate.NegotiateHandler[1]
      Incomplete Negotiate handshake, sending an additional 401 Negotiate challenge.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/1.1 GET https://localhost:7139/api/test/1 - - - 401 0 - 68.9309ms
dbug: Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[6]
      Connection id "0HMGSHHNJQ68O" received FIN.
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[10]
      Connection id "0HMGSHHNJQ68O" disconnecting.
dbug: Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[7]
      Connection id "0HMGSHHNJQ68O" sending FIN because: "The client closed the connection."
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[39]
      Connection id "0HMGSHHNJQ68P" accepted.
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[1]
      Connection id "0HMGSHHNJQ68P" started.
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[2]
      Connection id "0HMGSHHNJQ68O" stopped.
dbug: Microsoft.AspNetCore.Server.Kestrel.Https.Internal.HttpsConnectionMiddleware[3]
      Connection 0HMGSHHNJQ68P established using the following protocol: Tls12
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET https://localhost:7139/api/test/1 - -
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1001]
      1 candidate(s) found for the request path '/api/test/1'
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1005]
      Endpoint 'WebApp.Controllers.TestController.Get (WebApp)' with route pattern 'api/Test/{id}' is valid for the request path '/api/test/1'
dbug: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[1]
      Request matched endpoint 'WebApp.Controllers.TestController.Get (WebApp)'
dbug: Microsoft.AspNetCore.Authentication.Negotiate.NegotiateHandler[11]
      Negotiate error code: ClientError.
dbug: Microsoft.AspNetCore.Authentication.Negotiate.NegotiateHandler[10]
      The users authentication request was invalid.
      System.ComponentModel.Win32Exception (0x80090308): Das Token, das der Funktion übergeben wurde, ist ungültig.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/1.1 GET https://localhost:7139/api/test/1 - - - 400 0 - 40.9717ms
dbug: Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[6]
      Connection id "0HMGSHHNJQ68P" received FIN.
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[10]
      Connection id "0HMGSHHNJQ68P" disconnecting.
dbug: Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets[7]
      Connection id "0HMGSHHNJQ68P" sending FIN because: "The client closed the connection."
dbug: Microsoft.AspNetCore.Server.Kestrel.Connections[2]
      Connection id "0HMGSHHNJQ68P" stopped.

The first connection (0HMGSHHNJQ68N ) seems to be where I initially get the login-dialog in my browser where I enter my AD credentials. The other connections are then logged when I hit OK after entering my credentials. Sadly, I can't really figure out how to solve the problem based on those logs, but I am quite sure that the credentials are not provided correctly from the Angular app.

What am I doing wrong and/or how can I figure out the root cause of the problem?


Solution

  • In your CORS configuration, you try to open the API in a very broad way. Unfortunately, when dealing with requests containing an AUTHORIZATION header, you cannot use "*" to allow all hosts, but you need to specify the hosts. Also, you need to call AllowCredentials to accept requests with credentials:

    services.AddCors(setup =>
    {
      setup.AddDefaultPolicy(builder =>
      {
        builder.WithOrigins("http://localhost:4200")
          .AllowAnyHeader()
          .AllowAnyMethod()
          .AllowCredentials();
      });
    });
    

    Usually, the API is hosted under the same origin as the SPA in production, so these settings can be omitted in environments other than dev. Also, you might want to add a configuration setting instead of hard-coding http://localhost:4200.