aspnet / HttpSysServer

[Archived] A web server for ASP.NET Core based on the Windows Http Server API. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
106 stars 39 forks source link

Kerberos authentication not working using documented HTTP.sys setup on Windows #450

Closed Tratcher closed 6 years ago

Tratcher commented 6 years ago

From @karelz on April 27, 2018 17:23

From @narmafraz on April 27, 2018 16:22

Trying to setup a ASP .NET Core 2.0 REST API service on a Windows server (2012 R2) that authenticates users using Kerberos. Following official docs on setting up HTTP.sys: https://docs.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth?view=aspnetcore-2.1&tabs=aspnetcore2x#enable-windows-authentication-with-httpsys-or-weblistener

The minimal reproduction code that exhibits the issue:

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.HttpSys;
using Microsoft.Extensions.DependencyInjection;

namespace NetCoreHttpSys
{
    public class Program
    {
        public static void Main(string[] args)
        {
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseHttpSys(options =>
                {
                    options.Authentication.Schemes = AuthenticationSchemes.Negotiate;
                    options.Authentication.AllowAnonymous = false;
                    options.UrlPrefixes.Add("http://+:8777/");
                })
                .Build()
                .Run();
        }
    }

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);
        }

        public void Configure(IApplicationBuilder app)
        {
            app.Run(async context =>
            {
                var identity = context.User?.Identity;
                await context.Response.WriteAsync(
                    $"User={identity?.Name} " +
                    $"Authenticated={identity?.IsAuthenticated} " +
                    $"Type={identity?.AuthenticationType}");
            });
        }
    }

}

Project file:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
  </ItemGroup>

</Project>

Running this as user1 user on the server. The following SPN has been registered in AD against user1: HTTP/host1.domain.com:8777 This SPN is listed when I run setspn -L user1

But when I use Chrome browser to navigate to http://host1.domain.com:8777/ I can see by inspecting chrome://net-internals that the server is sending 401 with WWW-Authenticate: Negotiate twice and Chrome sends Kerberos tokens in the Authorization header twice back to the server and the third time the browser gets a 401 it pops up a user/pass challenge form.

Tried many different things without success and to make sure our setup/environment is not the issue I created minimal projects using other servers.

Minimal OWIN service running on .NET Framework 4.6.1 that successfully authenticates browser users using Kerberos using the same setup (running on same server as same user and same port):

using System;
using System.Net;
using System.Threading;
using System.Web.Http;
using Microsoft.Owin.Hosting;
using Owin;

namespace NetFrameworkOwin
{
    public class Program
    {
        public void RunWebApp(string[] urls)
        {
            using (WebApp.Start(new StartOptions(urls[0]), Configure))
            {
                Console.WriteLine("Started server, listening on " + urls[0]);
                new ManualResetEvent(false).WaitOne();
            }
        }

        public void Configure(IAppBuilder appBuilder)
        {
            var listener = (HttpListener)appBuilder.Properties["System.Net.HttpListener"];
            listener.AuthenticationSchemeSelectorDelegate = request => AuthenticationSchemes.Negotiate;

            appBuilder.Run(async context =>
            {
                var identity = context.Authentication?.User?.Identity;
                await context.Response.WriteAsync(
                    $"User={identity?.Name} " +
                    $"Authenticated={identity?.IsAuthenticated} " +
                    $"Type={identity?.AuthenticationType}");
            });
        }

        public static void Main(string[] args)
        {
            new Program().RunWebApp(args);
        }
    }
}

package.config file showing the dependency versions used:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net461" />
  <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net461" />
  <package id="Microsoft.AspNet.WebApi.Owin" version="5.2.3" targetFramework="net461" />
  <package id="Microsoft.Owin" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Host.HttpListener" version="3.1.0" targetFramework="net461" />
  <package id="Microsoft.Owin.Hosting" version="3.1.0" targetFramework="net461" />
  <package id="Newtonsoft.Json" version="6.0.4" targetFramework="net461" />
  <package id="Owin" version="1.0" targetFramework="net461" />
</packages>

Also tried using .NET Core 2.0 HttpListener and that also successfully authenticates browser users using Kerberos running in the same environment:

using System;
using System.Net;
using System.Text;

namespace NetCoreHttpListener
{
    class Program
    {
        public const string ResponseString = "Hello world";
        public static readonly byte[] Buffer = Encoding.UTF8.GetBytes(ResponseString);

        static void Main(string[] args)
        {
            try
            {
                UseHttpListener(args[0]);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

        static void UseHttpListener(string url)
        {
            var listener = new HttpListener { AuthenticationSchemes = AuthenticationSchemes.Negotiate };
            listener.Prefixes.Add(url);
            listener.Start();
            Console.WriteLine($"Listening on {url} with HttpListener...");

            while (true)
            {
                var context = listener.GetContext();
                var request = context.Request;
                Console.WriteLine($"Hit from {request.RemoteEndPoint.Address}...");
                var response = context.Response;
                response.ContentLength64 = Buffer.Length;
                var output = response.OutputStream;
                output.Write(Buffer, 0, Buffer.Length);
                output.Close();
            }
        }
    }
}

Project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

</Project>

So bit lost as to why the official documented method of using HTTP.sys is not working out of the box.

Any pointers on how to diagnose the issue further please?

Raising an issue to get help and see if this is an issue with .NET Core perhaps.​

[EDIT] Add syntax highlighting by @karelz

Copied from original issue: dotnet/corefx#29371

Copied from original issue: aspnet/Home#3103

Tratcher commented 6 years ago

From @karelz on April 27, 2018 17:23

If I understand your description correctly, you have problems only with ASP.NET Core using UseHttpSys. HttpListener is fine. In that case, I think we should start with ASP.NET Core team to look into it.

Tratcher commented 6 years ago

Kerberos is more difficult than most other forms of Windows Auth. There's a lot of domain setup involved. While I don't have an immediate answer, I do know what's different about HttpSys vs HttpListener and how that might require configuration changes.

HttpListener contains its own authentication logic that runs in user mode as the identity of the current process (a domain user?). Your domain kerberos setup is likely tied to that user account. HttpSys however delegates to kernel mode auth implementation provided by the underlying http.sys library. When you configure it with the domain it needs to be set up under the machine account, not the user account. I expect the SPN configuration to be tricker as well.

https://github.com/dotnet/corefx/issues/9996#issuecomment-308538004 covers how I got kerberos working with IIS, but that wasn't using kernel mode auth either. Someone's going to have to repeat that exercise for IIS with kernel mode as well as HttpSys and get it documented.

narmafraz commented 6 years ago

Thank you very much @Tratcher that is a very useful piece of information that helped resolve the issue.

Indeed we had registered the SPN against the user running the application rather than the host. So we registered against the host and it worked!

For anyone who is interested we registered setspn -S HTTP/host1.domain.com:8666 host1 and ran the service on port 8666 (changed ports so we don't get duplicate SPN issues) and we got HTTP.sys successfully doing the Kerberos handshake.

I don't think I saw this point about "HttpSys delegates to kernel mode" on the documentation so would perhaps be helpful to others to include it in .NET Core documentation if it does not exist already.

Thanks again, feel free to close this issue please.

Tratcher commented 6 years ago

Doc bug: https://github.com/aspnet/Docs/issues/6175