Monday, February 27, 2012

How to use Secure Store Id for Impersonation programmatically

Sometimes you run into issue that Application Pool Account doesn’t have access to Reporting Database or to another database but you should configure credential to access Reporting Database or other databases in your application. To achieve this Microsoft SharePoint 2010 has new service - Secure Store Service. You can provide account credential and set permission for users who can manage those credentials.

This article will describe how to configure Secure Store and how to work with it from code.

First step is configuring Secure Store Id:

  1. Navigate to Central Administration -> Application Management -> Manage service applications -> Secure Store Service

  2. Click New button on the ribbon, fill out form and click Next

  3. Click Next button without any changes

  4. Provide Target Application Administrators, Members and click OK

  5. Choose just created Secure Store in the list and click Set Credentials button on the ribbon

  6. Provide account name and password


You successfully completed Secure Store Id configuration. Next step is to read account credential programmatically. The following class read credential from provided Security Store Id and returns IdentityInfo.

public class IdentityInfo
{
    public string Domain { get; set; }
    public string Username { get; set; }
    public string Password { get; set; } 
}

public class SPSecureStoreService 
{
    public IdentityInfo GetCredentials(string targetApplicationId)
    {
        var credentials = GetWindowsCredentials(targetApplicationId);
        return ParseSecureStoreCredentials(credentials);
    }

    private IdentityInfo ParseSecureStoreCredentials(Dictionary<SecureStoreCredentialType, string> credentials)
    {
        if (!credentials.ContainsKey(SecureStoreCredentialType.WindowsUserName))
            throw new ArgumentNullException("credentials", "WindowsUserName field not defined");

        if (!credentials.ContainsKey(SecureStoreCredentialType.WindowsPassword))
            throw new ArgumentNullException("credentials", "WindowsPassword field not defined");
            
        string windowsUserName = credentials[SecureStoreCredentialType.WindowsUserName];
        string windowsPassword = credentials[SecureStoreCredentialType.WindowsPassword];

        var identityInfo = new IdentityInfo();
        identityInfo.Password = windowsPassword;

        if (string.IsNullOrEmpty(windowsUserName))
            throw new ArgumentNullException("credentials", "windowsUserName not defined");
            
        // treat domain\username
        if (windowsUserName.Contains("\\"))
        {
            var parts = windowsUserName.Split('\\');

            identityInfo.Domain = parts[0];
            identityInfo.Username = parts[1];
        }
        // treat username@domain
        else if (windowsUserName.Contains("@"))
        {
            var parts = windowsUserName.Split('@');

            identityInfo.Username = parts[0];
            identityInfo.Domain = parts[1];
        }
        else
        {
            identityInfo.Username = windowsUserName;
        }

        return identityInfo;
    }

    private Dictionary<SecureStoreCredentialType, string> GetWindowsCredentials(string targetApplicationId)
    {
        var serviceContext = SPServiceContext.Current;
        var secureStoreProvider = new SecureStoreProvider { Context = serviceContext };
        var credentialMap = new Dictionary<SecureStoreCredentialType, string>();

        using (var credentials = secureStoreProvider.GetCredentials(targetApplicationId))
        {
            var fields = secureStoreProvider.GetTargetApplicationFields(targetApplicationId);
            for (var i = 0; i < fields.Count; i++)
            {
                var field = fields[i];

                var credential = credentials[i];
                var decryptedCredential = ToClrString(credential.Credential);

                credentialMap.Add(field.CredentialType, decryptedCredential);
            }
        }

        return credentialMap;
    }

    private string ToClrString(SecureString secureString)
    {
        var ptr = Marshal.SecureStringToBSTR(secureString);

        try
        {
            return Marshal.PtrToStringBSTR(ptr);
        }
        finally
        {
            Marshal.FreeBSTR(ptr);
        }
    }
}

After you get credentials you will able to impersonate to the user. The following code does impersonation:

public class Impersonator : IDisposable
{
    #region Public methods.
    // ------------------------------------------------------------------

    public Impersonator(
        string userName,
        string domainName,
        string password)
    {
        ImpersonateValidUser(userName, domainName, password);
    }

    // ------------------------------------------------------------------
    #endregion

    #region IDisposable member.
    // ------------------------------------------------------------------

    public void Dispose()
    {
        UndoImpersonation();
    }

    // ------------------------------------------------------------------
    #endregion

    #region P/Invoke.
    // ------------------------------------------------------------------

    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern int LogonUser(
        string lpszUserName,
        string lpszDomain,
        string lpszPassword,
        int dwLogonType,
        int dwLogonProvider,
        ref IntPtr phToken);

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

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

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

    private const int LOGON32_LOGON_INTERACTIVE = 2;
    private const int LOGON32_PROVIDER_DEFAULT = 0;

    // ------------------------------------------------------------------
    #endregion

    #region Private member.
    // ------------------------------------------------------------------

    private void ImpersonateValidUser(
        string userName,
        string domain,
        string password)
    {
        WindowsIdentity tempWindowsIdentity = null;
        IntPtr token = IntPtr.Zero;
        IntPtr tokenDuplicate = IntPtr.Zero;

        try
        {
            if (RevertToSelf())
            {
                if (LogonUser(
                    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();
                    }
                    else
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    }
                }
                else
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }
            }
            else
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }
        }
        finally
        {
            if (token != IntPtr.Zero)
            {
                CloseHandle(token);
            }
            if (tokenDuplicate != IntPtr.Zero)
            {
                CloseHandle(tokenDuplicate);
            }
        }
    }

    private void UndoImpersonation()
    {
        if (impersonationContext != null)
        {
            impersonationContext.Undo();
        }
    }

    private WindowsImpersonationContext impersonationContext = null;

    // ------------------------------------------------------------------
    #endregion
}

Now you can work with your databases or other objects under user configured via Secure Store and no credentials are used directly in the code.

Enjoy!

1 comment:

  1. This looks suspiciously like the code used by the BDC, as revealed by .Net decompilers such as Reflector or dotPeek (I say, having recently done the same lookup).

    That said, you shouldn't need to use the Win32 API (advapi32 in this case) to do impersonation, just use the WindowsIdentity class and methods.

    ReplyDelete