3

I am trying to migrate an old OWIN self hosted WebApi that used this sort of thing

    var listener = (HttpListener)appBuilder.Properties["System.Net.HttpListener"];
    listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication;

To a new .NET Core 3.1 project. I have read about Auth

This is what my project file looks like

  <Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
      <TargetFramework>netcoreapp3.1</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
      <PackageReference Include="Microsoft.AspNetCore.Authentication.Negotiate" Version="3.1.2" />
      <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
      <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
      <PackageReference Include="System.Text.Json" Version="4.7.0" />
    </ItemGroup>
  </Project>

And this is what my launchsettings.json looks like

{
  "iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": false,
    "iisExpress": {
      "applicationUrl": "http://localhost:9600",
      "sslPort": 0
    }
  },
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/audit",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "use64Bit": true
    },
    "Audit.Core": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/audit",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:9600"
    },
    "Azure Dev Spaces": {
      "commandName": "AzureDevSpaces",
      "launchBrowser": true
    }
  }
}

And this is what my Startup.cs looks like

    using Microsoft.AspNetCore.Authentication.Negotiate;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Server.HttpSys;
    using Microsoft.AspNetCore.Server.IISIntegration;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using NLog;

    namespace xxxx
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }

            public IConfiguration Configuration { get; }

            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);
                services.AddControllers().AddNewtonsoftJson();
            }

            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {

                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }

                app.UseRouting();

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

                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                });
            }
        }
    }

And here is the Program.cs

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>()
                .UseHttpSys(options =>
                {
                    options.Authentication.Schemes = AuthenticationSchemes.NTLM;
                    options.EnableResponseCaching = false;
                    options.Authentication.AllowAnonymous = false;
                });
            })
            .ConfigureWebHost(config =>
            {
                 config.UseUrls("http://*:9600");
            });
}

And I have a custom filter that I need to grab the current users Identity from and verify it has a certain ActiveDirectory group associated with it. The basic usage is like this

    [ApiController]
    [Route("api/some")]
    [ControllerExceptionFilter]
    public class SomeController : ControllerBase
    {
        [HttpPost("add")]
        [ActiveDirectoryAuthorize("SomeGroup")]
        public IActionResult Add([FromBody]SomeEvent s)
        {
            var user = this.HttpContext.User.Identity;
            return Ok("cool");
        }
    }

Where the filter looks like this

    //https://stackoverflow.com/questions/31464359/how-do-you-create-a-custom-authorizeattribute-in-asp-net-core
    public class ActiveDirectoryAuthorizeAttribute : TypeFilterAttribute
    {
        public ActiveDirectoryAuthorizeAttribute(string groupMembership) : base(typeof(ActiveDirectoryAuthorizeFilter))
        {
            Arguments = new object[] { groupMembership };
        }
    }

    public class ActiveDirectoryAuthorizeFilter : IAuthorizationFilter
    {
        private string _groupMembership;
        public ActiveDirectoryAuthorizeFilter(string groupMembership)
        {
            _groupMembership = groupMembership;
        }

        public void OnAuthorization(AuthorizationFilterContext context)
        {
            try
            {
                Authenticate(context);
            }
            catch (InvalidWindowsUserException ex)
            {
                HandleUnauthorizedRequest(context);
            }

            catch (Exception ex)
            {
                HandleInternalServerError(context);
            }
        }

        protected void HandleUnauthorizedRequest(AuthorizationFilterContext context)
        {
            context.HttpContext.Response.Headers.Add("WWW-Authenticate", "NTLM");
            context.Result = new ContentResult
            {
                Content = "Unauthorized",
                StatusCode = (int)HttpStatusCode.Unauthorized
            };
        }

        private void HandleInternalServerError(AuthorizationFilterContext context)
        {
            context.HttpContext.Response.Headers.Add("WWW-Authenticate", "NTLM");
            context.Result = new ContentResult
            {
                Content = "Internal Server Error",
                StatusCode = (int)HttpStatusCode.InternalServerError
            };
        }

        private void Authenticate(AuthorizationFilterContext context)
        {
            var identity = context?.HttpContext?.User?.Identity;

            if (identity == null)
            {
                throw new InvalidWindowsUserException("Access denied");
            }

            EnsureAdmin(identity);
        }

        private void EnsureAdmin(IIdentity identity)
        {
            ......
        }
    }

So with all this in place I can launch POSTMAN and issue a NTLM request with a BAD password, I get a 401. Which is expected

enter image description here

So then I edit the POSTMAN request to put in the right password, and I get this, where I get a 200 and get the "cool" response from the controller shown above

enter image description here

All working as expected so far. But if I then change the current working POSTMAN (200 ok) request to use a BAD NTLM password again. I was expecting to see a 401, but instead the current user just shows up as being still authorized in my custom filter

enter image description here and I actually get a 200 OK

enter image description here

This behaviour is different to the OLD OWIN based WebApi. Which did recognize the following sequence

  1. NTLM bad password, 401 Unauthorized
  2. NTLM good password, 200 OK
  3. NTLM bad password, 401 Unauthorized

Is there something else I need to be setting somewhere? Does anyone have any clues on this?

Formalist
  • 171
  • 8
sacha barber
  • 1,754
  • 16
  • 29

1 Answers1

1

I experienced the same situation, and it seems to me that it's a Postman problem, like if it 'cached' the good password, and kept sending it even if you change it with a wrong one.

If you exit and re-enter Postman, and repeat the last request with the wrong password, you will get a 'correct' 401 Unauthorized, i.e.:

 1. NTLM bad password              -> 401 Unauthorized - correct
 2. NTLM good password             -> 200 OK           - correct
 3. NTLM bad password              -> 200 OK           - WRONG, as if Postman cached the good password
 4. Exit Postman - Re-enter Postman
 5. NTLM bad password (same as 3.) -> 401 Unauthorized - correct

Maybe it has something to do with NTLM autentication scheme, that imply a 'challenge', i.e., there is a 'negotiation' behind the scenes, with several HTTP calls that Postman handles transparently, but something goes wrong if you change the password.

EDIT: It is worth mentioning that the NTLM Authentication feature in Postman is currently in BETA

Formalist
  • 171
  • 8