'.NET Core WebAPI - Setting default authorization policy on Negotiate (Windows authentication) changes HttpContext.User type

If I setup Negotiate authentication (Windows/NTLM) in a .NET Core 5.0 or 3.1 WebAPI, and add a default authorization policy to require authenticated user, the type of HttpContext.User changes from System.Security.Principal.WindowsPrincipal to System.Security.Claims.ClaimsPrincipal.

I can successfully reproduce this issue by following the steps below.

Environment:

  • Visual Studio 2019 version 16.10.3
  • Windows 10 1909 (Build 18363.1621) on a corporate domain, using on-premise Active Directory
  • .NET Core 5.0.7/.NET Core 3.1.16
  • IIS Express using In Process hosting model

Steps to Reproduce:

  1. Create a new "ASP.NET Core WebAPI" project targeting .NET Core 5.0 or .NET Core 3.1. For what it's worth, I used the new project wizard in Visual Studio, not the dotnet command.

  2. Modify launchSettings.json to set IIS Express to enable Windows Authentication, and disable Anonymous Authentication. Also note I have disabled SSL by setting the port number to zero (not sure if this is relevant):

    {
      "iisSettings": {
        "windowsAuthentication": true,
        "anonymousAuthentication": false,
        "iisExpress": {
          "applicationUrl": "http://localhost:46007",
          "sslPort": 0
        }
      },
      "$schema": "http://json.schemastore.org/launchsettings.json",
      "profiles": {
        "IIS Express": {
          "commandName": "IISExpress",
          "launchBrowser": true,
          "launchUrl": "weatherforecast",
          "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        }
      }
    }
    
  3. Add the "Microsoft.AspNetCore.Authentication.Negotiate" NuGet package dotnet add package Microsoft.AspNetCore.Authentication.Negotiate (or with .NET Core 3.1: dotnet add package Microsoft.AspNetCore.Authentication.Negotiate --version 3.1.16)

  4. In Startup.cs, add the following using statement:

    using Microsoft.AspNetCore.Authentication.Negotiate;
    

    Add to ConfigureServices() before services.AddControllers();:

    services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
        .AddNegotiate();
    services.AddAuthorization();
    

    Before app.UseAuthorization(); in Configure(), add:

    app.UseAuthentication();
    
  5. To test, I added an [Authorize] attribute to the existing WeatherForecastController.cs Get() method, then added a simple line of code to easily debug the HttpContext.User object. This is my code:

    using Microsoft.AspNetCore.Authorization;
    
    // code removed for brevity
    
    [Authorize]
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        // set a debug breakpoint to inspect this value
        var user = HttpContext.User;
    
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
    
  6. Start Visual Studio debugger with the IIS Express profile, then inspect the HttpContext.User object. You will find the debugger reports this as System.Security.Principal.WindowsPrincipal, as expected.

  7. Modify Startup.cs to add the following default authorization policy to services.AddAuthorization() so the [Authorize] attribute requires an authenticated user.

    using Microsoft.AspNetCore.Authorization;
    
    services.AddAuthorization(cfg =>
    {
        cfg.DefaultPolicy = new AuthorizationPolicyBuilder(NegotiateDefaults.AuthenticationScheme)
            .RequireAuthenticatedUser()
            .Build();
    });
    
  8. With no additional changes, repeat step 6. This time inspecting the same HttpContext.User object, the debugger will report the type as System.Security.Claims.ClaimsPrincipal.

The issue

When the HttpContext.User is a WindowsPrincipal, you can use HttpContext.User.IsInRole(@"DOMAIN\Some_Group_Name"); to determine whether or not a user is a member of a specific Active Directory group, by name.

When HttpContext.User is a ClaimsPrincipal, the IsInRole() function always returns false for the same group name. Inspecting the claims, I do see where all the user's Active Directory groups are being pulled in, but can only be referenced by the SID, not the common name.

The only workaround I have found (Windows OS specific) is the following code:

new WindowsPrincipal((WindowsIdentity)HttpContext.User.Identity).IsInRole(@"DOMAIN\Some_Group_Name");

Why is this happening? Is this an issue with my configuration, code, and/or environment? How can I have a default authorization policy, but still have HttpContext.User as a WindowsPrincipal without the cast?



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source