10

I have the following code:

var baseUrl = "https://" + GetIdentityProviderHost(environment) + "/oauth2/authorize";
var query = $"?scope=openid&response_type=code&redirect_uri={redirectUrl}&client_id={clientId}";
var combinedUrl = baseUrl + query;

var currentUser = WindowsIdentity.GetCurrent(); 

await WindowsIdentity.RunImpersonated(currentUser.AccessToken, async() =>
{
    using (var client = new WebClient{ UseDefaultCredentials = true })
    {
        var response = client.DownloadString(combinedUrl);          
        Console.WriteLine(response);
    }
});

It basically constructs a URL and then calls it.

The call returns with a 401 (Unauthorized).

But if I take the combinedUrl and paste it into chrome or postman it works perfectly. That tells me that my call can work because Chrome is using my Windows Credentials to make the call.

I added the WindowsIdentity.RunImpersonated code to try to get around this issue. But it seems to not have had any effect.

How can I make a web call using Integrated Windows Authentication (IWA)?


Details:

If I run the following cURL command it works:

curl -L --negotiate -u : -b ~/cookiejar.txt "https://myIdp.domain.net/oauth2/authorize?scope=openid&response_type=code&redirect_uri=https://localhost:5001&client_id=my_client_id_here"

I am not sure how to replicate all that in C# code.

FYI: I have asked about this cURL command specifically in this question (since this question was focused on impersonation): Replicate cURL Command Using Redirect and Cookies in .Net Core 3.1

Vaccano
  • 78,325
  • 149
  • 468
  • 850
  • I tried to set up my project to reproduce your question. The problem is that I cannot "mock" your identity server. What is your identity server/auth endpoint? From my experience I used the ```HttpClient``` instead of ```WebClient``` to do requests maybe this can fix your problem (just a short guess)? – Martin Mar 31 '20 at 21:03
  • @Martin - My hope is that my question is not specific to my identity server. More about how to make a cookie to send out or something. I would share my identity server endpoint, but unfortunately my Identity Server is behind a firewall. It is a WSO2 Identity Server. I have tried it with HttpClient and got the same result. (I showed the use of `WebClient` because I saw another question that said that `WebClient` will include credentials: https://stackoverflow.com/a/12675503/16241 ) – Vaccano Mar 31 '20 at 21:34
  • Was just a blind guess :) ... next question: why is there a ```await``` in front of ```WindowsIdentity.RunImpersonated``` in the [documentation](https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonated?view=netcore-3.1) it is just ```void``` are you not using the default ```WindowsIdentity``` from ```System.Security.Principal``` namespace? – Martin Mar 31 '20 at 21:53
  • @Martin - Hmmm that is odd. The "Hover info" in LinqPad shows that it is using `System.Security.Principal`. But the docs link you sent is clearly showing it is only void. Not sure what to do about that... – Vaccano Mar 31 '20 at 22:01
  • Since you want to work on the current identity anyways, impersonation will probably not contribute to a solution. Is it any different if you directly configure credentials within the `WebClient` instead of relying on defaults? – grek40 Mar 31 '20 at 22:12
  • @Martin - If the `Action` passed into `RunImpersonated` is called awaited, then `RunImpersonated` returns a task. It returns `void` otherwise. – Vaccano Mar 31 '20 at 22:18
  • @grek40 - I tried to setup `WebClient`'s `Credentials` by following this answer, but still got 401: https://stackoverflow.com/a/1680866/16241 :( – Vaccano Mar 31 '20 at 22:41
  • Regarding *"works in chrome"*: does chrome even authenticate via NTLM or does it use something different (Negotiate)? – grek40 Apr 01 '20 at 05:23
  • @grek40 - I think Chrome, IE, and Edge do Integrated Windows Authentication by default. Firefox requires config to do it. – Vaccano Apr 01 '20 at 14:55
  • https://github.com/dotnet/runtime/issues/29935 – Hans Passant Apr 03 '20 at 12:20
  • can you confirm that `WindowsIdentity.GetCurrent()` is the correct credentials for authorization with the remote server? It seems to return the current account used for running the process, not the user authenticated with windows authentication (which is `(WindowsIdentity)context.User.Identity` if you use asp.net core). If you run a webserver, `WindowsIdentity.GetCurrent()` is likely the application pool identity – Khanh TO Apr 04 '20 at 05:03
  • @KhanhTO - It returns the user that is currently running. For me that is my user since I am testing this from my machine. NOTE: Further research has shown that this issue is due to cookie headers and redirects. cURL handles those, but .NET Core does not. – Vaccano Apr 06 '20 at 17:43

5 Answers5

5

I don't have a Windows box in front of me, so I cannot verify this thoroughly. But this seems to be a bit of a snake pit, based on the discussion e.g. here (especially from this comment down): https://github.com/dotnet/runtime/issues/24009#issuecomment-544511572

There seems to be various opinions on how to keep the Identity across an async call.

but if you look at the example in that comment,

app.Use(async (context, next) =>
{
    await WindowsIdentity.RunImpersonated(someToken, () => next());
});

it doesn't look like the func you send in as the second argument of WindowsIdentity.RunImpersonated should be async.

Have you tried:

var baseUrl = "https://" + GetIdentityProviderHost(environment) + "/oauth2/authorize";
var query = $"?scope=openid&response_type=code&redirect_uri={redirectUrl}&client_id={clientId}";
var combinedUrl = baseUrl + query;

var currentUser = WindowsIdentity.GetCurrent(); 

await WindowsIdentity.RunImpersonated(currentUser.AccessToken, () =>
{
    using (var client = new WebClient{ UseDefaultCredentials = true })
    {
        var response = client.DownloadString(combinedUrl);          
        Console.WriteLine(response);

    }
});

You can find the Microsoft docs on WindowsIdentity.RunImpersonated here: https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonated?view=netcore-3.1

Erik A. Brandstadmoen
  • 10,430
  • 2
  • 37
  • 55
  • 1
    Thank you for the response. I plugged that in and unfortunately still got a 401 response. (But still get a valid response when I put the `combinedUrl` in chrome. – Vaccano Mar 31 '20 at 22:14
  • 2
    The current implementation of RunImpersonated, does capture the identity in the execution context for resuming async code. But that isn't the problem here. It's more likely to be the dreaded "double hop issue". – Jeremy Lakeman Apr 01 '20 at 00:57
  • 1
    My question was incorrectly focused for my problem. I am going to award the bounty here as a thanks for the help (as this was the first answer). But for others that look at this, the issue for running a command like this is not as simple as a impersonated call. See https://stackoverflow.com/questions/60998324/replicate-curl-command-using-redirect-and-cookies-in-net-core-3-1 and https://stackoverflow.com/questions/60999574/how-to-i-make-a-curl-command-call-from-net-core-3-1-code for more information. – Vaccano Apr 06 '20 at 17:56
  • Thank you for the bounty reward. Sorry I wasn't able to solve your original issue. I haven't tried using impersonation to flow Windows Identities myself, so I just tried to read the docs thoroughly. Hope you are able to solve your original issue :) – Erik A. Brandstadmoen Apr 07 '20 at 13:23
3

Your impersonation code is ok. Are you writing a windows App or Asp.net core APP? There might be the user account issue that the user under you are executing code is standard user and cannot impersonate.Try domain user and give admin rights for testing. The other issue is that it can be used only in interactive mode.Like code for console app. // The following example demonstrates the use of the WindowsIdentity class to impersonate a user.
// IMPORTANT NOTE:
// This sample asks the user to enter a password on the console screen.
// The password will be visible on the screen, because the console window
// does not support masked input natively.

using System;  
using System.Runtime.InteropServices;  
using System.Security;  
using System.Security.Principal;  
using Microsoft.Win32.SafeHandles;  

public class ImpersonationDemo  
{  
    [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]  
    public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,  
        int dwLogonType, int dwLogonProvider, out SafeAccessTokenHandle phToken);  

    public static void Main()  
    {  
        // Get the user token for the specified user, domain, and password using the   
        // unmanaged LogonUser method.   
        // The local machine name can be used for the domain name to impersonate a user on this machine.  
        Console.Write("Enter the name of the domain on which to log on: ");  
        string domainName = Console.ReadLine();  

        Console.Write("Enter the login of a user on {0} that you wish to impersonate: ", domainName);  
        string userName = Console.ReadLine();  

        Console.Write("Enter the password for {0}: ", userName);  

        const int LOGON32_PROVIDER_DEFAULT = 0;  
        //This parameter causes LogonUser to create a primary token.   
        const int LOGON32_LOGON_INTERACTIVE = 2;  

        // Call LogonUser to obtain a handle to an access token.   
        SafeAccessTokenHandle safeAccessTokenHandle;  
        bool returnValue = LogonUser(userName, domainName, Console.ReadLine(),  
            LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,  
            out safeAccessTokenHandle);  

        if (false == returnValue)  
        {  
            int ret = Marshal.GetLastWin32Error();  
            Console.WriteLine("LogonUser failed with error code : {0}", ret);  
            throw new System.ComponentModel.Win32Exception(ret);  
        }  

        Console.WriteLine("Did LogonUser Succeed? " + (returnValue ? "Yes" : "No"));  
        // Check the identity.  
        Console.WriteLine("Before impersonation: " + WindowsIdentity.GetCurrent().Name);  

        // Note: if you want to run as unimpersonated, pass  
        //       'SafeAccessTokenHandle.InvalidHandle' instead of variable 'safeAccessTokenHandle'  
        WindowsIdentity.RunImpersonated(  
            safeAccessTokenHandle,  
            // User action  
            () =>  
            {  
                // Check the identity.  
                Console.WriteLine("During impersonation: " + WindowsIdentity.GetCurrent().Name);  
            }  
            );  

        // Check the identity again.  
        Console.WriteLine("After impersonation: " + WindowsIdentity.GetCurrent().Name);  
    }  
}
Jin Thakur
  • 2,711
  • 18
  • 15
3

.Net 5 running Impersonated (I am running this in Blazor)

I have spent a lot of time solving this and so I’m sharing my findings and my solution to hopefully help others avoid the pain!

Findings: On IIS the following code gets the IIS Appool account that the site is running under,

var currentUser = WindowsIdentity.GetCurrent();

So when you use the AccessToken, you are using the token for the wrong account.

I have also seen a lot of references to using an IHttpConextAccessor and lots of problems with this being null. This article from Microsoft suggests that this shouldn’t be used (Certainly in Blazor) MS-Docs

Solution: To get the user to impersonate use the AuthenticationStateProvider and get the user from this and cast to a WindowsIDentity to retrieve the AccessToken. This works in both a controller and a razor component. Inject the AuthenticationStateProvider and then in your method use the following code:

    var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
    var user = authState.User;
    var userToImpersonate = (WindowsIdentity)user.Identity;
    
    await WindowsIdentity.RunImpersonatedAsync(userToImpersonate.AccessToken, async () => 
  {
    
      // Your Code in here
    
  }

The windows Impersonation is only for Windows so if you want to suppress the Visual studio warnings surround the code with the following:

#pragma warning disable CA1416 // Validate platform compatibility
...
#pragma warning restore CA1416 // Validate platform compatibility
Ren
  • 59
  • 5
  • If I try this, I get a castException: 'Unable to cast object of type 'System.Security.Claims.ClaimsIdentity' to type 'System.Security.Principal.WindowsIdentity" Do you have an idea why this happens? – N.B. Oct 21 '21 at 11:50
  • Edit: This error gets thrown when windowsAuthentication is disabled :) – N.B. Oct 21 '21 at 12:26
  • Yes you need to have the Windows Authentication enabled to be able to get the Windows Identity. – Ren Nov 02 '21 at 11:57
1

Sadly, I wasn't able to reproduce your problem, impersonation is working fine for me when using this code:

WindowsIdentity identity = WindowsIdentity.GetCurrent();

using (identity.Impersonate())
{
    HttpWebRequest request = (HttpWebRequest) WebRequest.Create("https://my-address");
    request.UseDefaultCredentials = true;

    HttpWebResponse response = (HttpWebResponse) request.GetResponse();
}

I tested this with .NET Framework only, but since you already tried to setup Credentials manually, I guess it's not the .NET Core impersonation problem that was mentioned in one of the comments.

So my guess is that the problem is related to the address you are trying to access.

What might be the problem is the redirection, which I was not able to test, but you may want to try solution from this answer. You would use request.AllowAutoRedirect = false since the default value is true and in that case authorization header is cleared on auto-redirects (MSDN AllowAutoRedirect Property).

Other than that, you might also want to try using request.ImpersonationLevel = TokenImpersonationLevel.Delegation (MSDN ImpersonationLevel Property) or request.PreAuthenticate = true (MSDN PreAuthenticate Property).

As I said, I was not able to reproduce problem, so these are just some ideas which might (or might not) work for you...

Nemanja Banda
  • 796
  • 2
  • 14
  • 23
0

I had similar situation. I have a mvc and web api. both use windows authentication and I need to pass the current mvc's user's windows identity to api to authenticate. I use .net 5. I did this and it works. so in startup.cs, I added this

services.AddHttpClient("somename", c =>
            {
                c.BaseAddress = new Uri(Configuration.GetValue<string>("baseURL"));
            })
            .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
            {
                UseDefaultCredentials = true
            }); 

when I need to use httpclient to call the API. I did this:

IPrincipal p = _httpContextAccessor.HttpContext.User;
                HttpResponseMessage result=null;
                if(p.Identity is WindowsIdentity wid)
                {
                    await WindowsIdentity.RunImpersonated(wid.AccessToken, async () =>
                     {
                         result = await _client.GetAsync("APIController/Action");
                     });
                }
wxm146
  • 151
  • 1
  • 3