Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WindowsIdentity impersonation does not achieve delegation #17828

Closed
Tratcher opened this issue Jul 11, 2016 · 36 comments
Closed

WindowsIdentity impersonation does not achieve delegation #17828

Tratcher opened this issue Jul 11, 2016 · 36 comments
Assignees
Milestone

Comments

@Tratcher
Copy link
Member

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
Copy link

@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
Copy link
Member Author

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

@jruckert
Copy link

jruckert commented Jul 12, 2016

Example of how we are attempting to use the impersonation

var callerIdentity = localSecurityService.CurrentUser() as WindowsIdentity;

using (callerIdentity.Impersonate())
{
   return ((DbContext)Current).SaveChanges();
}

@joshfree
Copy link
Member

cc: @bartonjs

@Tratcher
Copy link
Member Author

@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
Copy link
Member

CIPop commented Jul 12, 2016

@bartonjs PTAL
/cc @davidsh

@jruckert
Copy link

jruckert commented Jul 12, 2016

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-
Copy link

mfe- commented Jul 13, 2016

Maybe related to #16842 Open System.Security.Principal.WindowsIdentity

@brentschmaltz
Copy link

@Tratcher can you point me to the repo code?

@Tratcher
Copy link
Member Author

Tratcher commented Sep 9, 2016

@DerekStrickland
Copy link

Is there any update on this issue?

@CIPop
Copy link
Member

CIPop commented Apr 4, 2017

[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
Copy link
Contributor

davidsh commented Apr 4, 2017

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
Copy link
Contributor

davidsh commented Apr 4, 2017

cc: @karelz

@jruckert
Copy link

jruckert commented Apr 4, 2017

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

@jruckert
Copy link

jruckert commented Apr 4, 2017

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

@karelz
Copy link
Member

karelz commented Apr 5, 2017

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

@CIPop
Copy link
Member

CIPop commented Apr 5, 2017

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?

@CIPop CIPop closed this as completed Apr 5, 2017
@gperrego
Copy link

gperrego commented May 30, 2017

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
Copy link
Member Author

Tratcher commented Jun 12, 2017

@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
Copy link

gperrego commented Jun 13, 2017

@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
Copy link
Member

karelz commented Jun 13, 2017

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
Copy link
Member

karelz commented Jun 13, 2017

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.

@karelz karelz reopened this Jun 13, 2017
@Tratcher
Copy link
Member Author

Tratcher commented Jun 14, 2017

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:

  • 3 machines on a private network all joined to the same domain. (All Windows Server 2012 R2)
      1. a domain controller + back-end web server
      1. a middle tier web server
      1. a client machine
  • On the domain controller go into active directory, find machine number 2, and enable "Trust this computer for delegation to any service (Kerberos only)"
  • On the web server create your web site and configure auth as follows:
    • Disable Anonymous
    • Enable Windows
      • Under advanced settings disable kernel-model auth
      • Under providers remove "Negotiate", add "Negotiate:Kerberos", and move it to the top of the list.
    • I used the default ApplicationPoolIdentity for the app pool
  • On the domain controller set up a back-end web site with the same authentication configuration.
  • On the client machine open Internet Explorer, Internet Options, Security, Local intranet, Sites, Advanced, and add the FQDN for your web server (e.g. http://web.domain.net). You can also do this on the web server for local testing.
    • Make sure to log into this client machine with a different user account so it's easy to tell that the user is flowing all the way through.
  • No SPN setup should be required as long you use the FQDN whenever you access the web server.

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
Copy link

gperrego commented Jun 20, 2017

@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
Copy link
Member Author

@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
Copy link
Member Author

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

@davidsh
Copy link
Contributor

davidsh commented Nov 22, 2017

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

@Tratcher
Copy link
Member Author

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
Copy link
Member

@Tratcher, is this still an issue?

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

@Tratcher
Copy link
Member Author

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

@davidsh
Copy link
Contributor

davidsh commented Mar 22, 2019

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

@davidsh davidsh assigned davidsh and unassigned brentschmaltz Mar 22, 2019
@Tratcher
Copy link
Member Author

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

@gperrego
Copy link

gperrego commented Mar 25, 2019 via email

@dotnet dotnet deleted a comment from VaniKulkarni Apr 3, 2019
@wpbrown
Copy link

wpbrown commented Apr 3, 2019

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
Copy link
Contributor

davidsh commented Apr 3, 2019

I've solved this mystery.

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

@Tratcher Tratcher closed this as completed Apr 3, 2019
@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 3.0 milestone Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 30, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests