Friday, September 28, 2007

Certificate Access Error in a IIS hosted WCF Service

The problem appears when a WCF Service hosted in an IIS tries to load a certificate from the Windows Certificates Store with the account of the Application Pool where the service runs, and the account’s profile is not previously loaded. When a user logs on interactively, the system automatically loads the user's profile. If a service or an application impersonates a user, the system does not load the user's profile. Therefore, the service or application should load the user's profile with LoadUserProfile.

When this happens the operation throws the following exception:

System.Security.Cryptography.CryptographicException: The system cannot find the file specified.

See http://support.microsoft.com/kb/939761 Microsoft Knowledge Base Article for detailed information.

A workaround to this problem is to load the Application Pool Identity Account´s profile before the service call is executed. Placing the code in the Application_Start() method on the Global.asax of the IIS host will solve the problem (see http://msdn2.microsoft.com/en-us/library/aa374341.aspx for detailed information).

Here is the code:

        private ProfileManager.PROFILEINFO profile;

 

        protected void Application_Start(object sender, EventArgs e)

        {

            bool retVal = false;

            // Need to duplicate the token. LoadUserProfile needs a token with

            // TOKEN_IMPERSONATE and TOKEN_DUPLICATE.

            const int SecurityImpersonation = 2;

            dupeTokenHandle = DupeToken(WindowsIdentity.GetCurrent().Token, SecurityImpersonation);

            if (IntPtr.Zero == dupeTokenHandle)

            {

                throw new Exception("Unable to duplicate token.");

            }

 

            // Load the profile.

            profile = new ProfileManager.PROFILEINFO();

            profile.dwSize = 32;

            //Domain\User

            profile.lpUserName = @"MyDomain\UserName";

            retVal = ProfileManager.LoadUserProfile(dupeTokenHandle, ref profile);

 

            if (!retVal)

            {

                throw new Exception("Error loading user profile. " + Marshal.GetLastWin32Error());

            }

        }

 

        protected void Application_End(object sender, EventArgs e)

        {

            ProfileManager.UnloadUserProfile(WindowsIdentity.GetCurrent().Token, profile.hProfile);

            CloseHandle(dupeTokenHandle);

        }

 

        private IntPtr DupeToken(IntPtr token, int Level)

        {

            IntPtr dupeTokenHandle = new IntPtr(0);

            bool retVal = DuplicateToken(token, Level, ref dupeTokenHandle);

            if (false == retVal)

            {

                return IntPtr.Zero;

            }

            return dupeTokenHandle;

        }

 

    }

 

    internal class ProfileManager

    {

        [DllImport("Userenv.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]

        internal static extern bool LoadUserProfile(IntPtr hToken, ref PROFILEINFO lpProfileInfo);

        [DllImport("Userenv.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]

        internal static extern bool UnloadUserProfile(IntPtr hToken, IntPtr hProfile);

 

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]

        public struct PROFILEINFO

        {

            public int dwSize;

            public int dwFlags;

            public String lpUserName;

            public String lpProfilePath;

            public String lpDefaultPath;

            public String lpServerName;

            public String lpPolicyPath;

            public IntPtr hProfile;

        }

 

    }

}

 

An alternative workaround consists in creating a Windows Service account that loads at system start-up using the Application Pool Service Identity.

Thanks to JavierA for helping me to find the solution.

5 comments:

Javier said...

Marvelous is the definition for this article. I was trying to solve this issue for weeks and here's the solution. I will keep on reading your articles!

Unknown said...

I got similiar problem, DPAPI requires the user profile to be loaded. Your solution doesn't work for me for some reason - the app pool process configured with custom identity doesn't have SeBackupPrivilege and SeRestorePrivilege. W/o those LoadUserProfile doesn't work (as per MSDN). I'm wondering if there's something else that needs to be done on the configuration side to get it working? My environment: IIS6/ASP.NET 2.0/W2K3 Server.

Unknown said...

Never mind, found my problem: after granting an account all those privileges system needs a reboot, otherwise even though account has them, they are not assigned to the worker process.

Will 保哥 said...

The code is uncompleted. I couldn't build for CloseHandle and DuplicateToken method. Can you tell how to import these methods?

Unknown said...

I think these declarations are missing:

[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public extern static bool DuplicateToken(IntPtr ExistingTokenHandle,
int SECURITY_IMPERSONATION_LEVEL, ref IntPtr DuplicateTokenHandle);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public extern static bool CloseHandle(IntPtr handle);