dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.27k stars 4.73k forks source link

WindowsIdentity impersonation does not achieve delegation #17828

Closed Tratcher closed 4 years ago

Tratcher commented 8 years ago

Setup: Client: IE or Chrome Server: Asp.Net Core 1.0 via IIS or WebListener, with Windows Auth enabled.

Scenario: A client logs into a web app using windows credentials. The web app in turn impersonates that user to make outgoing HttpClient requests also using windows credentials.

Expected: The outgoing HttpClient request should made using the impersonated user's credentials.

Actual: The outgoing HttpClient request is made using the web apps default credentials.

The scenario works fine when running Asp.Net Core 1.0 on .NET 4.6, it only fails on .NET Core.

In both the IIS and WebListener scenarios the WindowsIdentity is constructed from an existing handle from a native API: https://github.com/aspnet/IISIntegration/blob/ed85f504d8da633202b3fec5fdf11e8d6153d447/src/Microsoft.AspNetCore.Server.IISIntegration/IISMiddleware.cs#L112. Since this works with .NET 4.6 apps we assume the original handle is valid and something is wrong inside WindowsIdentity or HttpClient.

HttpClient does work with impersonated WindowsIdentities created locally (http://stackoverflow.com/questions/7710538/impersonate-with-username-and-password). Only the delegation scenario appears to be broken.

@brentschmaltz @CIPop

Ping me directly for repro code, there are a lot of different parts.

jruckert commented 8 years ago

@Tratcher we can also see the same issue when using the EF Core framework. (Its not passing the credentials to SQL Server).

Browser -> Web App -> SQL Server (this always shows using SQL Profiler as the dotnet identity process).

Tratcher commented 8 years ago

@jruckert are you impersonating the user before calling SQL? Asp.net core does not do this impersonation for you.

jruckert commented 8 years ago

Example of how we are attempting to use the impersonation

var callerIdentity = localSecurityService.CurrentUser() as WindowsIdentity;

using (callerIdentity.Impersonate())
{
   return ((DbContext)Current).SaveChanges();
}
joshfree commented 8 years ago

cc: @bartonjs

Tratcher commented 8 years ago

@jruckert that doesn't make sense, the WindowsIdentity.Impersonate() API isn't even in .NET Core, it's only in the full framework. You need to call RunImpersonated instead.

CIPop commented 8 years ago

@bartonjs PTAL /cc @davidsh

jruckert commented 8 years ago

I agree its a bit weird, here is our framework definition in the project.json file.

Note: our current definition of WindowsIdentity does not have RunImpersonated, only Impersonate. I'm going to look into this now.

"frameworks": {
    "net451": {},
    "netstandard1.3": {
      "imports": [
        "dotnet5.4",
        "dnxcore50",
        "portable-net452+win8"
      ],
      "dependencies": {
        "System.Runtime.Extensions": "4.1.0-rc2-24027",
        "System.Security.Claims": "4.0.1-rc2-24027",
        "System.Security.Principal": "4.0.1-rc2-24027",
        "System.Linq.Queryable": "4.0.1-rc2-24027",
        "NETStandard.Library": "1.5.0-rc2-24027"
      }
    }
  }
mfe- commented 8 years ago

Maybe related to dotnet/runtime#16842 Open System.Security.Principal.WindowsIdentity

brentschmaltz commented 8 years ago

@Tratcher can you point me to the repo code?

Tratcher commented 8 years ago

Repro sample branch: https://github.com/aspnet/IISIntegration/blob/tratcher/delegate/samples/DelegationSample/Startup.cs#L143

DerekStrickland commented 7 years ago

Is there any update on this issue?

CIPop commented 7 years ago

[edited as it's the source of many mistakes]

The original comment was describing a bug, not the right pattern. Please see: https://github.com/dotnet/corefx/issues/24977

davidsh commented 7 years ago

Tentatively marking as 2.0: This is a behavior difference between .Net Framework and .Net Core (to the point where if WinHttpHandler is ran on Framework the scenario works as expected).

So, this tells me that the problem is not the HTTP stack (HttpClient using WinHTTP). Rather, it is a process / delegation problem in .NET Core.

davidsh commented 7 years ago

cc: @karelz

jruckert commented 7 years ago

I'm confident that this is working now (we are using WindowsIdentity.RunImpersonated) with Kerberos Delegation.

jruckert commented 7 years ago

Here is the middleware we created: https://github.com/novaworksau/impersonate

karelz commented 7 years ago

@CIPop did you try it on latest 2.0? If yes, any idea why @jruckert thinks it works now?

CIPop commented 7 years ago

Thanks @jruckert for letting us know! That saves us a lot of time 👍

did you try it on latest 2.0?

@karelz no, my changes were only triage. Closing now since an external customer reports the scenario works fine. This is in-line with @davidsh's hypothesis of this being a process/delegation issue in .NET Core, which makes sense since the same WinHTTP code worked fine in Desktop.

@Tratcher, can you please give this a try on 2.0 and reopen if you can still repro?

gperrego commented 7 years ago

We are struggling with this also, I'm trying to understand if this was verified in .net CORE 1.1? Seems link @jruckert jruckert has it working, we have implemented the middleware but we are still not there yet. Have others had success implementing the middleware?

Tratcher commented 7 years ago

@jruckert That impersonation middleware doesn't actually work, please take it down. Actually most of the samples posted so far have made the same mistake.

You're using the overload T RunImpersonated<T>(SafeAccessTokenHandle safeAccessTokenHandle, Func<T> func). The T you're returning is the Task for the async action. The problem is that the impersonation is revoked when RunImpersonated exits, which may be before your Task completes.

To really make this work the CLR needs to add a separate RunImpersonatedAsync method that does not revoke the impersonation until the async Task is completed. Even then, making sure the identity flows properly across threads will be problematic.

gperrego commented 7 years ago

@CIPop, @Tratcher @jruckert @karelz

Can you please reopen this issue.

Do you know if there is a planned solution for this? Doesn't seem like @Tratcher was comfortable with how to solve the problem but maybe its on someone else's radar?

Then I would ask if it is possibly set for a 2.0 release? I know that's a lot to ask but unfortunately moved forward with a solution believing this is possible and changing the approach at this point in time would be rather expensive.

Thanks for you time,

Thank you, Greg

karelz commented 7 years ago

I would suggest to create a new bug. It is confusing for me to understand what is the ask here. .NET Core 2.0 is basically done, unless it is something super-super important, blocking major scenarios, it won't get it. If it is super-important, I would expect clear description what's missing, why and why we cannot workaround.

karelz commented 7 years ago

We had a discussion with @Tratcher and @CIPop. The key problem is that setting up the repro is extremely involved (you need your own DomainController and 2 additional machines on the network). We do not know in which component the bug is yet. We will have to debug that once we have the setup.

@Tratcher is trying to resurrect his setup from last year, let's see how it goes.

Realistically, I don't think it will meet the 2.0 bar. However, we can start the discussion about servicing patch, once we know where the problem is and once we understand how much is the scenario important to how many customers.

Tratcher commented 7 years ago

Good news, I've gotten this to work now for ASP.NET Core 1.1 and 2.0 preview1 (and ASP.NET 4.5 as a baseline). Setting up Kerberos for IIS was the hard part. Here are the highlights:

This setup can be used to test both ASP.NET 4.5 and ASP.NET Core apps.

Here is the default.aspx file I placed in my back-end website for testing all scenarios:

<%@ Page Language="C#" %>
<script runat=server>
protected System.Security.Principal.WindowsIdentity GetUser()
{
    return (System.Security.Principal.WindowsIdentity)Context.User.Identity;
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

<asp:LoginView ID="LoginView1" runat="server">

    <LoggedInTemplate>
      Hi <%= Context.User.Identity.Name %> <br>
      State: <%= GetUser().ImpersonationLevel %> <br>
    </LoggedInTemplate>

    <AnonymousTemplate>
      Hi Guest         
    </AnonymousTemplate>

</asp:LoginView>

    </div>
    </form>
</body>
</html>

Here's the default.aspx file I placed on my middle tier server for testing ASP.NET 4.5 scenarios:

<%@ Page Language="C#" %>
<script runat=server>
protected System.Security.Principal.WindowsIdentity GetUser()
{
    return (System.Security.Principal.WindowsIdentity)Context.User.Identity;
}
protected string GetUserState()
{
    using (GetUser().Impersonate())
    {
        return System.Security.Principal.WindowsIdentity.GetCurrent().ImpersonationLevel.ToString();
    }
}
protected string GetSubSection()
{
    using (GetUser().Impersonate())
    {
        return new System.Net.WebClient() { UseDefaultCredentials = true }.DownloadString("http://Win2012r2-DC.testing.net/");
    }
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

<asp:LoginView ID="LoginView1" runat="server">

    <LoggedInTemplate>
      Hi <%= Context.User.Identity.Name %> <br>
      State: <%= GetUser().ImpersonationLevel %> <br>
      Impersonated State: <%= GetUserState() %> <br>
      Next Hop: <%= GetSubSection() %> <br>
    </LoggedInTemplate>

    <AnonymousTemplate>
      Hi Guest         
    </AnonymousTemplate>

</asp:LoginView>

    </div>
    </form>
</body>
</html>

Results:

Hi TESTING\TestUser 
State: Delegation 
Impersonated State: Delegation 
Next Hop:  
Hi TESTING\TestUser 
State: Delegation 

And here's an ASP.NET Core repro app you can build and publish to the middle tier server. I tested this with netcoreapp1.0, netcoreapp2.0, net461, AspNetCore 1.0, 1.1, 2.0-preview1 and 2.0-dev packages.

AuthApp.csproj

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

  <PropertyGroup>
    <TargetFramework>netcoreapp1.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="1.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="1.0.0" />
  </ItemGroup>

</Project>

program.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace AuthApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            new WebHostBuilder()
                .UseKestrel()
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();
    }
}

startup.cs

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Linq;
using System.Threading.Tasks;
using System.Security.Principal;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace AuthApp
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
try
{
                await context.Response.WriteAsync("Hello World!<br>");
                var user = (WindowsIdentity)context.User.Identity;

                await context.Response.WriteAsync($"User: {user.Name}<br>");
                await context.Response.WriteAsync($"State: {user.ImpersonationLevel}<br>");
/*
                await context.Response.WriteAsync($"Downstream:<br>"
                    + "WebClient: <br>" + new WebClient() { UseDefaultCredentials = true }
                         .DownloadString("http://win2012r2-dc.testing.net"));
*/
                await context.Response.WriteAsync($"Downstream:<br>"
                    + "HttpClient: <br>" + await new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true }) 
                         .GetStringAsync("http://win2012r2-dc.testing.net"));

                await context.Response.WriteAsync($"Impersonating:<br>");
#if NET461
                using (user.Impersonate())
#else
                WindowsIdentity.RunImpersonated(user.AccessToken, () =>
#endif
                {
                     var useri = WindowsIdentity.GetCurrent();
                     var text = $"User: {useri.Name}<br>State: {useri.ImpersonationLevel}<br>";
/*
                     text += "WebClient: <br>" + new WebClient() { UseDefaultCredentials = true }
                         .DownloadString("http://win2012r2-dc.testing.net");
*/
                     text += "HttpClient: <br>" + new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true }) 
                         .GetStringAsync("http://win2012r2-dc.testing.net").Result;

                     var bytes = System.Text.Encoding.UTF8.GetBytes(text);
                     context.Response.Body.Write(bytes, 0 , bytes.Length);
                }
#if !NET461
                );
#endif
}
catch (Exception e)
{
                await context.Response.WriteAsync(e.ToString());  
}
            });
        }
    }
}

Results:

Hello World!
User: TESTING\TestUser
State: None
Downstream:
HttpClient: 

Hi TESTING\WIN2012R2-WEB$ 
State: Delegation 

Impersonating:
User: TESTING\TestUser
State: Impersonation
HttpClient: 

Hi TESTING\TestUser 
State: Delegation 

@brentschmaltz The only remaining mystery is why the ImpersonationLevel is reported as Impersonation on the middle tier rather than Delegation. Delegation is working, we see the identity passed to back-end site.

Note: The FREB logs do show the requests as TokenImpersonationLevel ImpersonationDelegate. https://docs.microsoft.com/en-us/iis/troubleshoot/using-failed-request-tracing/troubleshooting-failed-requests-using-tracing-in-iis

gperrego commented 7 years ago

@Tratcher Thanks for all of your work so far! One question on your set up. Is this step really required?

"back-end web site with the same authentication configuration."

I hadn't seen that as a required step when looking at how to set up impersonation. That was always done on the Middle Tier Web Server but I hadn't seen that as a requirements on the Site/App that hosts the Web Service that gets called where you want the impersonation to work. In my case I'm attempting to impersonate a call to TFS's rest API as the end user, so are you saying we may need to update the TFS Services' web site security settings? In general I have seen enabling Kerberos on the service accounts but I hadn't seen changing the Service itself.

I've also had seen that if you don't set up a SPN you can't enable Kerberos on the service accounts, seems like you were able to do that though?

Thanks, Greg

Tratcher commented 7 years ago

@gperrego I didn't spend a lot of time tinkering with the auth settings on the back-end site, I mirrored them for simplicity. It's likely they are not as strict as those on the middle tier, but you still need Windows auth enabled and Anonymous disabled.

I didn't have to set any special SPNs because I was always using the machine FQDN, which is registered as an SPN by default. You need additional SPNs if your machine name doesn't match your site name, like when you scale across multiple machines.

Tratcher commented 7 years ago

Filed https://github.com/dotnet/corefx/issues/24977 for async impersonation.

davidsh commented 6 years ago

@Tratcher Can this issue be closed now? There doesn't seem to be any more actionable work on this issue.

Tratcher commented 6 years ago

There's one minor issue left to investigate:

@brentschmaltz The only remaining mystery is why the ImpersonationLevel is reported as Impersonation on the middle tier rather than Delegation. Delegation is working, we see the identity passed to back-end site.

stephentoub commented 5 years ago

@Tratcher, is this still an issue?

@brentschmaltz, this is assigned to you. Are you working on this?

Tratcher commented 5 years ago

Hard to say, it takes a long time to set up a repro environment (private domain controller, two web servers, and client).

davidsh commented 5 years ago

I think this is a problem in HttpClient. Looking at related issues and the code, it is not passing the right flags for delegation requests to the Negotiate SSPI. See: https://github.com/dotnet/corefx/issues/34697#issuecomment-475453845

Tratcher commented 5 years ago

@davidsh delegation was working, last I checked. Only something was wrong with WindowsIdentity reporting the current impersonation level.

gperrego commented 5 years ago

Chris,

As for our issue at my client we found that we think it had something to do with cross domain authentication issues. Let me see if I can dig up the old emails and find it.

Thanks, Greg


From: VaniKulkarni notifications@github.com Sent: Monday, March 25, 2019 4:02 AM To: dotnet/corefx Cc: gperrego; Mention Subject: Re: [dotnet/corefx] WindowsIdentity impersonation does not achieve delegation (#9996)

test

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/dotnet/corefx/issues/9996#issuecomment-476108196, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AV-QZ9jma54QwuRpZxXHfehxcUgQpKZCks5vaJCVgaJpZM4JJ5lq.

wpbrown commented 5 years ago

Only something was wrong with WindowsIdentity reporting the current impersonation level.

I've solved this mystery. "Delegation" is only reported as the impersonation level for unconstrained delegation. The incoming ticket to the application must have ok_as_delegate. Constrained delegation tickets don't have this flag, but still can be used to acquire tickets for SPNs whitelisted in AD. Constrained delegation was a protocol extension added later and I'm guessing they didn't want to change the Win32 API to add a new impersonation level for it.

This is the case straight from the win32 APIs, regardless of .NET.

dotnet/corefx#2>     Client: user1 @ CORP.BEAGLELAB.SPACE
        Server: HTTP/testappmid.corp.beaglelab.space @ CORP.BEAGLELAB.SPACE
        KerbTicket Encryption Type: RSADSI RC4-HMAC(NT)
        Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize

{"IncomingUser":{"Message":"Hello","Name":"beaglelab\\user1","IsAuthenticated":true,"AuthenticationType":"Negotiate","ImpersonationLevel":"Delegation"}

vs.

dotnet/corefx#1>     Client: user1 @ CORP.BEAGLELAB.SPACE
        Server: HTTP/testappmid.corp.beaglelab.space @ CORP.BEAGLELAB.SPACE
        KerbTicket Encryption Type: RSADSI RC4-HMAC(NT)
        Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize

{"IncomingUser":{"Message":"Hello","Name":"beaglelab\\user1","IsAuthenticated":true,"AuthenticationType":"Negotiate","ImpersonationLevel":"Impersonation"}
davidsh commented 5 years ago

I've solved this mystery.

Does this mean that everything is working as expected and we can close this issue now?