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 account2019s 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 AccountB4s 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://msdn.microsoft.com/en-us/library/aa374341.aspx for detailed information).
Here is the code:
private ProfileManager.PROFILEINFO profile;
A0
protected void Application_Start(object sender, EventArgs e)
{
A0A0A0 bool retVal = false;
A0A0A0 // Need to duplicate the token. LoadUserProfile needs a token with
A0A0A0 // TOKEN_IMPERSONATE and TOKEN_DUPLICATE.
A0A0A0 const int SecurityImpersonation = 2;
A0A0A0 dupeTokenHandle = DupeToken(WindowsIdentity.GetCurrent().Token, SecurityImpersonation);
A0
A0A0A0 if (IntPtr.Zero == dupeTokenHandle)
A0A0A0 {
A0A0A0A0A0A0A0 throw new Exception("Unable to duplicate token.");
A0A0A0 }
A0
A0A0A0 // Load the profile.
A0A0A0 profile = new ProfileManager.PROFILEINFO();
A0A0A0 profile.dwSize = 32;
A0A0A0 //Domain\User
A0A0A0 profile.lpUserName = @"MyDomain\UserName";
A0A0A0 retVal = ProfileManager.LoadUserProfile(dupeTokenHandle, ref profile);
A0
A0A0A0 if (!retVal)
A0A0A0 {
A0A0A0A0A0A0A0 throw new Exception("Error loading user profile. " + Marshal.GetLastWin32Error());
A0A0A0 }
}
A0
protected void Application_End(object sender, EventArgs e)
{
A0A0A0 ProfileManager.UnloadUserProfile(WindowsIdentity.GetCurrent().Token, profile.hProfile);
A0A0A0 CloseHandle(dupeTokenHandle);
}
A0
private IntPtr DupeToken(IntPtr token, int Level)
{
A0A0A0 IntPtr dupeTokenHandle = new IntPtr(0);
A0A0A0 bool retVal = DuplicateToken(token, Level, ref dupeTokenHandle);
A0A0A0
A0A0A0 if (false == retVal)
A0A0A0 {
A0A0A0A0A0A0A0 return IntPtr.Zero;
A0A0A0 }
A0
A0A0A0 return dupeTokenHandle;
}
A0
internal class ProfileManager
{
A0A0A0 [DllImport("Userenv.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
A0A0A0 internal static extern bool LoadUserProfile(IntPtr hToken, ref PROFILEINFO lpProfileInfo);
A0A0A0 [DllImport("Userenv.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
A0A0A0 internal static extern bool UnloadUserProfile(IntPtr hToken, IntPtr hProfile);
A0
A0A0A0 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
A0A0A0 public struct PROFILEINFO
A0A0A0 {
A0A0A0A0A0A0A0 public int dwSize;
A0A0A0A0A0A0A0 public int dwFlags;
A0A0A0A0A0A0A0 public String lpUserName;
A0A0A0A0A0A0A0 public String lpProfilePath;
A0A0A0A0A0A0A0 public String lpDefaultPath;
A0A0A0A0A0A0A0 public String lpServerName;
A0A0A0A0A0A0A0 public String lpPolicyPath;
A0A0A0A0A0A0A0 public IntPtr hProfile;
A0A0A0 }
}
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.