Search code examples
c#.netwindows-serviceswindows-authenticationimpersonation

Windows Service impersonate User with AccessToken


I have two applications, one is an Windows Service (written in .NET 6) and the other is an Winform Application (also written in .NET 6). The Winform application is in the user's autostart and is reduced to the system tray. The Windows Service is set to automatic start.

The Windows Service and the Winform Application are connected via Pipes. Now I want to access my (asp.net core) api which uses Windows Authentification with my Windows Service. To do that I thought I would need Impersonation. And as I don't know the credentials of my current logged in user I thought i can simply pass the WindowsIdentity.Token via pipes to my Service.

like this:

// ... other server pipe stuff ...
var writer = new StreamWriter(serverPipe) { AutoFlush = true };
await writer.WriteLineAsync(Convert.ToString(WindowsIdentity.GetCurrent().Token));

and use the token in my Windows Service like this:


IntPtr.TryParse(Convert.ToString(TOKEN_FROM_WINFORM_APPLICATION), out var winAuthToken);
return await WindowsIdentity.RunImpersonatedAsync(new SafeAccessTokenHandle(winAuthToken), async () =>
{
    using var client = new HttpClient(new HttpClientHandler
    {
        UseDefaultCredentials = true, // send winAuth token
    });

    var content = new StringContent(body, Encoding.UTF8, "application/json");
    var url = $"{baseUrl}{endpoint}?{query}";

    var response = requestMethod switch
    {
        RequestMethod.Get => await client.GetAsync(url),
        RequestMethod.Post => await client.PostAsync(url, content),
        _ => throw new ArgumentOutOfRangeException(nameof(requestMethod), requestMethod, null)
    };

    return await response.Content.ReadAsStringAsync();
});

This approach results in an System.ArgumentException: "Invalid token for impersonation - it cannot be duplicated. exception.

I've also tried to duplicate the token, but I still get the same error:

[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int DuplicateToken(IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken);

IntPtr token = default;
if (DuplicateToken(WindowsIdentity.GetCurrent().Token, 3, ref token) != 0)
{
    await writer.WriteLineAsync(Convert.ToString(token));
}

The microsoft docs and all I could find on stackoverflow only shows how to use the client credentials to impersonate.

I've also seen some approches by using the OpenProcessApi, but I have no idea which ProcessHandle and DesiredAccess I would need.

How can I use the token of the current logged in user to perform the Windows Authentification on my WebApi?

Thanks for any help!

Edit: okay I found out (in this answer) that I could use OpenProcessApi with the processId of the logged in user to gain access, but as I already have a Winform application running it would be easier to gain it there, wouldn't it?

I've also found out that it is definetly wrong to use WindowsIndentity.Token as it is pointed out in the official docs that it won't work as a userToken.


Solution

  • I found a very helpful class on Github, which implements the win32 api method WTSQueryUserToken which does exactly what I was looking for. It returns an AccessToken for the currently logged in user. I just needed to switch the TokenType to TOKEN_TYPE.TokenImpersonation.

    Now my code looks the following (I renamed ProcessExtensions.cs to Win32Api.cs):

    var userToken = IntPtr.Zero;
    Win32Api.GetSessionUserToken(ref userToken);
    
    var identity = new WindowsIdentity(userToken);
    
    // impersonate current logged in user        
    return await WindowsIdentity.RunImpersonatedAsync(identity.AccessToken, async () =>
    {
        using var client = new HttpClient(new HttpClientHandler
        {
            UseDefaultCredentials = true, // send winAuth token
        });
    
        var content = new StringContent(body, Encoding.UTF8, "application/json");
        var url = $"{baseUrl}{endpoint}?{query}";
    
        var response = requestMethod switch
        {
            RequestMethod.Get => await client.GetAsync(url),
            RequestMethod.Post => await client.PostAsync(url, content),
            _ => throw new ArgumentOutOfRangeException(nameof(requestMethod), requestMethod, null)
        };
    
        return await response.Content.ReadAsStringAsync();
    });
    
    
    

    and this works like a charm! I hope that this will help future reader.