Thursday, April 08, 2010

Impersonating a specific windows identity

Alternate Title: How to get a WCF web service to use an alternate fixed identity to access a resource (such as a database).

By default, WCF will ignore the “<identity impersonate="true" userName="domain\username" password="xxxxxxx" />”. This is because even though the WCF service and ASP.net web site are using the same web.config, the ASP.net process is using only the settings under the System.Web node for its configuration and the WCF service is using the System.ServiceModel node for its configuration.

So how do you get WCF to use a particular identity to access a resource such as a database?

The problem I had was that I had a public facing website which needed to access a database. The database allowed only for windows authentication. Everytime the website called the webservice, the connection.Open command would fail as the user accessing the database was invalid.

There are a few ways to get around this issue.

1. Use the correct Identity as the ASPNet app pool identity. I believe this is the identity that WCF will use when attempting to access any resource. Unfortunately, if you are using IIS under XP then you dont have access to the AppPool identity. (which was my case). Using the correct identity is the best method to enable the scenario we are working on here.

2. If (1) is not possible, then you can get WCF to accept your “identity impersonate” settings that have been set in the system.web node.

The first thing to do is add the following line under the <system.servicemodel> node of your web.config.

<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />

As you can see, what we are specifying here is that WCF will have to play nice with ASP.Net. The next thing you need to do is, in each of your service implementations, you need to define the following attribute at the class level.

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]

This lets WCF know that you are indeed alright with your service code running under ASP.Net compatibility mode. Now whenever your end up using a protected resource (like open a database connection that is set up to using integrated identity), it will use the identity specified in the “<identity impersonate="true" userName="domain\username" password="xxxxxxx" />” node.

I dont like this solution, because you are opening up WCF to ASP.Net, when in reality WCF is a technology that doesnt care about ASP.Net. So its a shame to make the 2 speak to each other if the only reason we are enabling it is so that we can use the settings in the identity node of the system.web node.

But some times you have got to do what you got to do and in those cases, the above might be a reasonable solution.

3. Use the LogonApi to set the “WindowsImpersonationContext” for the operation that needs to use a different identity to access the resource.

This is a nice solution too, except for the fact that you need to use PInvoke to access the LogonApi (which sits in the advapi32.dll). The reason this is nice is that if you need to use multiple identities for the different resources that you need to access, then the only way to do that is using this method. (Another bad part to this option is that you need to store the username password somewhere, so that you can use it to perform the logon – so be sure to think about encrypting this information).

The code needed is the following:

using System;
using System.Collections.Generic;
using System.Linq;

using System.Runtime.InteropServices;
using System.Security.Principal;

/// <summary>
/// Summary description for LogonAPI
/// </summary>
public class LogonAPI
{
    public const int LOGON32_LOGON_INTERACTIVE = 2;
    public const int LOGON32_PROVIDER_DEFAULT = 0;
    [DllImport("advapi32.dll")]
    public static extern int LogonUserA(String lpszUserName,
        String lpszDomain,
        String lpszPassword,
        int dwLogonType,
        int dwLogonProvider,
        ref IntPtr phToken);
    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int DuplicateToken(IntPtr hToken,
        int impersonationLevel,
        ref IntPtr hNewToken);

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool RevertToSelf();

    [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
    public static extern bool CloseHandle(IntPtr handle);


    public static bool impersonateValidUser(String userName, String domain, String password, ref WindowsImpersonationContext impersonationContext)
    {
        WindowsIdentity tempWindowsIdentity;
        IntPtr token = IntPtr.Zero;
        IntPtr tokenDuplicate = IntPtr.Zero;

        if (RevertToSelf())
        {
            if (LogonUserA(userName, domain, password, LOGON32_LOGON_INTERACTIVE,
                LOGON32_PROVIDER_DEFAULT, ref token) != 0)
            {
                if (DuplicateToken(token, 2, ref tokenDuplicate) != 0)
                {
                    tempWindowsIdentity = new WindowsIdentity(tokenDuplicate);
                    impersonationContext = tempWindowsIdentity.Impersonate();
                    if (impersonationContext != null)
                    {
                        CloseHandle(token);
                        CloseHandle(tokenDuplicate);
                        return true;
                    }
                }
            }
        }
        if (token != IntPtr.Zero)
            CloseHandle(token);
        if (tokenDuplicate != IntPtr.Zero)
            CloseHandle(tokenDuplicate);
        return false;
    }
    public static void undoImpersonation(WindowsImpersonationContext impersonationContext)
    {
        impersonationContext.Undo();
    }
}

And to use it

WindowsImpersonationContext impersonationContext = null;
if (LogonAPI.impersonateValidUser("username", "domain", "password", ref impersonationContext))
{
    //do work here
    LogonAPI.undoImpersonation(impersonationContext);
}
else
{
   //impersonation failed
}

Are there any other ways to do the above? Is there a better way to do it? (preferably through configuration?)

2 comments:

Dam Pun said...

yes, i think above given solution is good but not better, so, i was wondering to know if it can be done through configuration for even desktop application like wpf or windows form

Dam Pun said...

yes, the way that you have provided is good but not better, so, i really wondering to know, it above could be done through configurationSetting for desktop application like wpf or windows form