Closed davidmatson closed 5 years ago
Do you actually need Windows Authentication or just seamless login?
3.0 will come with it's own kerberos authentication (although it'll be limited to username only, no groups or roles).
There's always ADFS, with the bonus there that you can move to a cloud hosted container, or indeed a linux container without changing anything.
Hey, Barry! For this scenario, I just need HTTP Negotiate or Kerberos auth (seamless login) that provides group membership for seamless on-prem authorization.
@Tratcher
Note #3022 was able to work around the issue so it's not completely blocked.
These are windows containers, right? Have you tried the new 3.0 auth features?
Looking through that issue, it sounded like the workaround was to use a different feature collection part for OnComplete, though I'm not exactly sure what that would look like. Do you have any details? (I didn't see how to from a quick look through the thread).
These are Windows containers. We're still on 2.x and I'm rather not move to 3.0 yet if possible. Either way, from what Barry said above it sounded like its auth features wouldn't give us group membership information, which we need in this case. Let me know if that's incorrect though.
The first entry in #3022 says "I copied the OwinFeatureCollection code into my own project and commented out the throw... and model binding now works"
The groups are available on Windows, just not Linux.
I thought that wasn't really an option:
Removing the NotSupportException and silently ignoring OnCompleted isn't really an option, it would cause memory leaks.
Good to hear about groups in 3.0. Will they work when running under a gMSA? Any suggestions for what do while on 2.x?
Thanks.
OwinFeatureCollection couldn't implement OnCompleted at a middleware layer, it would fire too early. At a IServer layer though you control the lifetime of the request and could fire OnCompleted once everything is really completed.
Good to hear about groups in 3.0. Will they work when running under a gMSA?
No idea. I'm curious to hear what you find.
Any suggestions for what do while on 2.x?
No, 2.x didn't have all the infrastructure to build this. You could backport most of our Negotiate package on Windows but we added several server features in 3.0 to help track and clean up the connection auth state. Not sure if you'd be able to work around that.
Thanks, Chris. I'll probably investigate a custom FeatureCollection (not using OWIN) as a 2.x workaround for now - any suggestions on how to do a full custom IServer, or is there any documentation?
I tried doing a full implementation of FeatureCollection for HttpListener, but that was hours worth of tricky work including request thread pool handling. I ended up just wrapping the IHttpResponseFeature from OWIN and customizing the handling of OnComplete (and then calling these callbacks later). I'd rather find a way to get rid of OWIN, but implementing IServer seems too much work to be worth a custom implementation.
I'm guessing I should be calling the complete callbacks after awaiting application.ProcessRequestAsync and before calling application.DisposeContext - is that correct?
Sounds right.
In case someone else needs the 2.x workaround, here's what I used:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace Servers
{
class CompletableHttpResponseFeature : IHttpResponseFeature
{
readonly IHttpResponseFeature inner;
readonly List<CallbackRegistration> completedRegistrations = new List<CallbackRegistration>();
public CompletableHttpResponseFeature(IHttpResponseFeature inner)
{
Debug.Assert(inner != null);
this.inner = inner;
}
public int StatusCode { get => inner.StatusCode; set => inner.StatusCode = value; }
public string ReasonPhrase { get => inner.ReasonPhrase; set => inner.ReasonPhrase = value; }
public IHeaderDictionary Headers { get => inner.Headers; set => inner.Headers = value; }
public Stream Body { get => inner.Body; set => inner.Body = value; }
public bool HasStarted => inner.HasStarted;
public void OnCompleted(Func<object, Task> callback, object state)
{
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
completedRegistrations.Add(new CallbackRegistration(callback, state));
}
public void OnStarting(Func<object, Task> callback, object state)
{
inner.OnStarting(callback, state);
}
public async Task CompleteAsync()
{
foreach (CallbackRegistration registration in completedRegistrations)
{
Func<object, Task> callback = registration.Callback;
object state = registration.State;
await callback.Invoke(state);
}
}
struct CallbackRegistration
{
readonly Func<object, Task> callback;
readonly object state;
public CallbackRegistration(Func<object, Task> callback, object state)
{
Debug.Assert(callback != null);
this.callback = callback;
this.state = state;
}
public Func<object, Task> Callback => callback;
public object State => state;
}
}
}
HttpListenerServer snippet:
server = OwinServerFactory.Create(async (environment) =>
{
IFeatureCollection readOnlyFeatureCollection = new OwinFeatureCollection(environment);
IFeatureCollection mutableFeatureCollection = new FeatureCollection(readOnlyFeatureCollection);
IHttpResponseFeature owinHttpResponseFeature = readOnlyFeatureCollection.Get<IHttpResponseFeature>();
CompletableHttpResponseFeature completableHttpResponseFeature =
new CompletableHttpResponseFeature(owinHttpResponseFeature);
// See: https://github.com/aspnet/AspNetCore/issues/13861
mutableFeatureCollection.Set<IHttpResponseFeature>(completableHttpResponseFeature);
TContext context = application.CreateContext(mutableFeatureCollection);
try
{
await application.ProcessRequestAsync(context);
}
catch (Exception exception)
{
await completableHttpResponseFeature.CompleteAsync();
application.DisposeContext(context, exception);
throw;
}
await completableHttpResponseFeature.CompleteAsync();
application.DisposeContext(context, null);
}, properties);
I tested the 3.x auth features (Negotiate package), and it looks like it's working fine with a gMSA in a docker container!
In case we need to test again in the future, here's the 3.x auth test code I used: Test.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Negotiate" Version="3.0.0-preview9.19424.4" />
</ItemGroup>
</Project>
Program.cs:
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Hosting;
static class Program
{
static void Main()
{
IHostBuilder hostBuilder = new HostBuilder()
.ConfigureLogging(logging => logging.AddConsole())
.ConfigureWebHostDefaults(webHost => webHost.UseStartup<Startup>());
using (IHost host = hostBuilder.Build())
{
host.Run();
}
}
}
Startup.cs:
using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
HomeController.cs:
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine($"User.Identity.IsAuthenticated: {User.Identity.IsAuthenticated}");
builder.AppendLine($"User.Identity.AuthenticationType: {User.Identity.AuthenticationType}");
builder.AppendLine($"User.Identity.Name: {User.Identity.Name}");
builder.AppendLine("User.Claims");
builder.AppendLine();
foreach (Claim claim in User.Claims)
{
builder.AppendLine($"\tType: {claim.Type}");
builder.AppendLine($"\tValue: {claim.Value}");
builder.AppendLine($"\tIssuer: {claim.Issuer}");
if (claim.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid")
{
SecurityIdentifier securityIdentifier = new SecurityIdentifier(claim.Value);
NTAccount ntAccount = (NTAccount)securityIdentifier.Translate(typeof(NTAccount));
builder.AppendLine($"\tNTAccount.Value: {ntAccount.Value}");
}
builder.AppendLine();
}
return Content($"<pre>{builder.ToString()}</pre>", new MediaTypeHeaderValue("text/html"));
}
}
Describe the bug
ASP.NET Core 2.1+ doesn't have a way to do Windows Authentication inside a Docker container, starting with version 2.1. In 2.0.x, using OWIN as a workaround (with HttpListener) worked.
To Reproduce
Use OWIN with HttpListener, and enable Windows Authentication using a gMSA in a Docker container.
Expected behavior
Http requests succeed
Actual behavior
Http requests fail with NotSupportedException
Additional context
Earlier, https://github.com/aspnet/HttpSysServer/issues/333 was closed as won't fix, so I'm unable to use HttpSysServer for this scenario and had to use a custom IServer implementation using OWIN's FeatureCollection as a workaround. But #3022 was also closed as won't fix for 2.1+, so that solution no longer works.
If I need to do Windows Authentication in a Docker container, what approach is recommended? The only documentation I'm seeing for implementing IServer shows using OWIN's FeatureCollection , and any other implementation seems non-trivial (and I couldn't find documentation/an example of how to do so).