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

2

6

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?

Grandiose answered 12/4, 2022 at 12:2 Comment(5)
Instead of using the webpack proxy, can you try to access the API directly? Can you show your proxy.conf.js? Also, this answer might help: https://mcmap.net/q/1778886/-how-to-configure-ng-serve-proxy-with-windows-authentication-and-https/642579Choosy
@Choosy Yes, trying to access the API directly works fine. For example, I can just access it directly in my browser via the URL https://localhost:7139/api/test/1, enter my AD credentials and it works just fine. Therefore, I think that the Angular app does not yet know how to deal with the credentials, but I have no clue how to configure it to do that. I also figured out how to get more useful logging out of Kestrel. I added the logs and the content of my proxy.conf.json to my question above. Do you have an idea how I can make this work?Grandiose
In our case, we didn't use the proxy - that worked just fine. What happens if you change the request in Angular to this.http.get<string>('https://localhost:7139/api/test/1', ...?Choosy
@Choosy When I try that I get the following error: Access to XMLHttpRequest at 'https://localhost:7139/api/test/1' from origin 'https://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.Grandiose
Hi @Chris, it should be builder.WithOrigins("https://localhost:4200") instead of builder.WithOrigins("http://localhost:4200")Galantine
C
3

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.

Choosy answered 12/4, 2022 at 12:20 Comment(9)
Do I still need the UseCors() call here, or is that unnecessary when using your code?Grandiose
Yes, you still need UseCors.Choosy
Without a policy name (so just app.UseCors())?Grandiose
Yes, just app.UseCors();. The sample sets the default policy, so I don't think you have to specify a policy. At least it works like this in our app.Choosy
Thanks! I just tried that but unfortunately I am still getting a 400 Bad Request error with my example GET call. If I would get more details about the error I would at least have a starting point for debugging the problem, but right now I am just clueless.Grandiose
You could try to move AddControllers after AddAuthentication and AddAuthorization.Choosy
Sadly, that also does not seem to make a difference.Grandiose
Let us continue this discussion in chat.Choosy
Thanks a lot again for the help! I accepted your answer despite the actual solution being in the chat and not in the answer itself. If anyone here encounters a similar problem like I did, please check out the chat discussion for more information on how to solve this problem.Grandiose
C
2

I had the same problem. The problem is with the SPA proxy not passing www-authenticate header.

I solved with this solution: https://mcmap.net/q/1778886/-how-to-configure-ng-serve-proxy-with-windows-authentication-and-https

Certie answered 6/2, 2023 at 10:57 Comment(1)
This is a key part of making windows auth work with Angular. Yet I have not found it anywhere except here.Unclose

© 2022 - 2024 — McMap. All rights reserved.