I need to digitally sign PDF documents on a web server running ASP.NET Core 2.2 in IIS. The web app is running with a service user and the impersonation shall be done in the code. The problem is, I can't access the users certificates via the X509Store class. I tried to create a minimal example which only impersonates a different user and outputs the certificates in the console. I ran it as an administrator but no certifiactes were found.
Permissions I granted in the "Local Group Policy Editor" (gpedit.msc) under "Computer Configuration > Windows Settings > Security Settings > Local Policies > User Rights Management":
Act as part of the operating system: Added ServiceUser
Allow log on locally: Added UserToBeImpersonated
Log on as batch job: Added UserToBeImpersonated
Create a token object: Added ServiceUser
Replace a process level token: Added ServiceUser
Adjust Memory Quotas for a process: Added ServiceUser
For the impersonation I used the SimpleImpersonation from https://github.com/mj1856/SimpleImpersonation and extended it to also load the user profile like this:
public static void RunAsUser(UserCredentials credentials, LogonType logonType, Action action)
{
// this method tells Windows to dynamically determine where to look for the HKEY_CURRENT_USER registry hive,
// rather than using the cached location from when the process was initially invoked
RegDisablePredefinedCache();
using (var tokenHandle = credentials.Impersonate(logonType))
using (var profileToken = credentials.ImpersonateUserProfile(tokenHandle.DangerousGetHandle()))
{
RunImpersonated(tokenHandle, _ => action());
}
}
internal UserProfileToken ImpersonateUserProfile(IntPtr tokenHandle)
{
var tokenDuplicate = IntPtr.Zero;
// Not sure if the token needs to be duplicated or not
if (DuplicateToken(tokenHandle, 2, ref tokenDuplicate) == 0)
HandleError(tokenHandle);
// Load User profile
var profileInfo = new ProfileInfo();
profileInfo.dwSize = Marshal.SizeOf(profileInfo);
profileInfo.lpUserName = _username;
profileInfo.dwFlags = 1;
// LoadUserProfile() failed
if (!LoadUserProfile(tokenDuplicate, ref profileInfo))
HandleError(tokenDuplicate);
// LoadUserProfile() failed - HKCU handle was not loaded
if (profileInfo.hProfile == IntPtr.Zero)
HandleError(tokenDuplicate);
return new UserProfileToken() { ProfileInfo = profileInfo, Token = tokenDuplicate };
}
public class UserProfileToken : IDisposable
{
public ProfileInfo ProfileInfo { get; set; }
public IntPtr Token { get; set; }
~UserProfileToken()
{
Dispose();
}
private bool isDisposed = false;
public void Dispose()
{
if (isDisposed) return;
isDisposed = true;
UnloadUserProfile(Token, ProfileInfo.hProfile);
}
}
[DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool LoadUserProfile(IntPtr hToken, ref ProfileInfo lpProfileInfo);
[DllImport("Userenv.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool UnloadUserProfile(IntPtr hToken, IntPtr lpProfileInfo);
[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)]
public static extern int RegDisablePredefinedCache();
The main code for the testing looks like this:
var credentials = new Impersonator.UserCredentials(domain, username, password);
Impersonator.RunAsUser(credentials, Impersonator.LogonType.Interactive, () =>
{
Console.WriteLine($"Current user: {WindowsIdentity.GetCurrent().Name}");
var store = new X509Store(StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
if (store.Certificates.Count == 0)
{
Console.WriteLine("No Certificates found.");
}
else
{
var certCount = 0;
foreach (var cert in store.Certificates)
{
Console.WriteLine($"[{certCount}] Subject: {cert.Subject}");
Console.WriteLine($"[{certCount}] Issuer: {cert.Issuer}");
certCount++;
}
}
});
Unfortunately no certificats are found when the same code (without impersonation) shows two certificates on the target users machine.
Another interesting thing I found on the server is that the users profile directory under C:\Users<username> has not the users name but some Chinese? characters. When checking the user profiles in the system settings the username is correct though.
Seems like the impersonation (with loading the profile) does not trigger the Auto-Enrollment for receiving the certificate from the certificate authority. This must be done seperately with something like this:
using CERTENROLLLib;
public static void EnrollUserCertificateByTemplate(string templateName, string subjectName, string friendlyName = null)
{
var enrollment = new CX509Enrollment();
// Set target store
enrollment.InitializeFromTemplateName(X509CertificateEnrollmentContext.ContextUser, templateName);
var request = enrollment.Request;
var innerRequest = request.GetInnerRequest(InnerRequestLevel.LevelInnermost);
var innerRequestPkcs10 = innerRequest as IX509CertificateRequestPkcs10;
// Set the subject name
var distinguishedName = new CX500DistinguishedName();
distinguishedName.Encode(subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);
innerRequestPkcs10.Subject = distinguishedName;
// Set the friendly name
if (friendlyName != null) enrollment.CertificateFriendlyName = friendlyName;
// Enroll for the certificate into MY store if it is successfully issued by CA
enrollment.Enroll();
}
Additionaly you need to check if there is already a valid certificate available in the users certificate store. Otherwise you add a certificate everytime you impersonate (and execute the code above).
Anothr thing I needed to do was giving Administrative rights to the executing user. All the other permissions posted in my question could be remove.
Related
I can't seem to start a process as another user when using impersonation under .Net Core.
I'm running this script in Linqpad running as User1 and trying to launch a program as User2.
At first, the impersonation seems to work (the Console.Writeline()s on the current user change correctly from User1 to User2 in the RunImpersonated() Method). However, the process always runs as User1.
This is one of many tests I'm doing to validate that RunImpersonated() works (this originally stems from issues in an ASP.Net Core App trying to impersonate the current user). This is the simplest reproducible example I could find.
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,
int dwLogonType, int dwLogonProvider, out SafeAccessTokenHandle phToken);
void Main()
{
string domainName = "myDomain";
string userName = "User2";
string passWord = "User2Password";
const int LOGON32_PROVIDER_DEFAULT = 0;
//This parameter causes LogonUser to create a primary token.
const int LOGON32_LOGON_INTERACTIVE = 2;
// Call LogonUser to obtain a handle to an access token.
SafeAccessTokenHandle safeAccessTokenHandle;
bool returnValue = LogonUser(userName, domainName, passWord,
LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
out safeAccessTokenHandle);
if (false == returnValue)
{
int ret = Marshal.GetLastWin32Error();
Console.WriteLine("LogonUser failed with error code : {0}", ret);
throw new System.ComponentModel.Win32Exception(ret);
}
Console.WriteLine("Did LogonUser Succeed? " + (returnValue ? "Yes" : "No"));
// Check the identity.
Console.WriteLine("Before impersonation: " + WindowsIdentity.GetCurrent().Name);
// Note: if you want to run as unimpersonated, pass
// 'SafeAccessTokenHandle.InvalidHandle' instead of variable 'safeAccessTokenHandle'
WindowsIdentity.RunImpersonated(
safeAccessTokenHandle,
// User action
() =>
{
// Check the identity.
Console.WriteLine("During impersonation: " + WindowsIdentity.GetCurrent().Name);
Directory.GetFiles(#"C:\TMP\").Dump();
var pi = new ProcessStartInfo
{
WorkingDirectory = #"C:\TMP\",
FileName = #"C:\TMP\TestUser.exe"
};
var proc = Process.Start(pi);
proc.WaitForExit();
}
);
// Check the identity again.
Console.WriteLine("After impersonation: " + WindowsIdentity.GetCurrent().Name);
}
If you don't specify username and password, Process.Start will uses the token for the calling process, not the impersonation token.
Looking into source code of Process.Start:
If you pass user name and password it uses CreateProcess
If you don't specify username and password, it calls CreateProcessWithLogon
If the calling process is impersonating another user, the new process
uses the token for the calling process, not the impersonation token.
To run the new process in the security context of the user represented
by the impersonation token, use the CreateProcessAsUser or
CreateProcessWithLogonW function.
Without passing username and password the process always runs on security context of the original process owner. If you want to run the process under the context of another user:
If you have username and password: use CreateProcessWithLogon
If you have a to the primary token that represents a user use CreateProcessAsUser
There are multiple impersonation levels. Documentation states that you cannot use thread impersonation token to start the process - primary token will always be used:
The system uses the primary token of the process rather than the
impersonation token of the calling thread in the following situations:
If an impersonating thread calls the CreateProcess function, the new
process always inherits the primary token of the process.
Given you don't have user's password and you want to use impersonation token instead to start a process, unfortunately, the answer is - you can't do that.
You can impersonate caller for other operations though (RPC, COM, FS...).
A while back I used the following code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;
using System.Text;
using System.Windows;
using Microsoft.Win32.SafeHandles;
namespace ZZZ
{
partial class User : IDisposable
{
private string m_domain;
private string m_user;
private string m_pass;
private WindowsIdentity user;
private SafeTokenHandle safeTokenHandle;
private WindowsImpersonationContext impContext;
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool LogonUser(
string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out SafeTokenHandle phToken);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private extern static bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool RevertToSelf();
//For the current user
public User()
{
user = WindowsIdentity.GetCurrent();
}
// For custom user
public User(string domain, string user, string password, bool doImepsonate = false)
{
m_domain = domain;
m_user = user;
m_pass = password;
if (doImepsonate) this.Impersonate();
}
// If it's intended to incorporate this code into a DLL, then demand FullTrust.
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
public void Impersonate()
{
if (impContext != null) throw new ImpersonationException();
try
{
// Get the user token for the specified user, domain, and password using the unmanaged LogonUser method.
// The local machine name can be used for the domain name to impersonate a user on this machine.
const int LOGON32_PROVIDER_DEFAULT = 0;
//This parameter causes LogonUser to create a primary token.
const int LOGON32_LOGON_INTERACTIVE = 2;
// Call LogonUser to obtain a handle to an access token.
bool returnValue = LogonUser(
m_user,
m_domain,
m_pass,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
out safeTokenHandle);
if (returnValue == false)
{
int ret = Marshal.GetLastWin32Error();
throw new Win32Exception(ret);
}
using (safeTokenHandle)
{
// Use the token handle returned by LogonUser.
user = new WindowsIdentity(safeTokenHandle.DangerousGetHandle());
impContext = user.Impersonate();
}
}
catch (Exception ex)
{
MessageBox.Show("Exception occurred:\n" + ex.Message);
}
}
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
private void Quit()
{
if (impContext == null) return;
impContext.Undo();
safeTokenHandle.Dispose();
}
#endregion
internal IEnumerable<string> Groups
{
get
{
return user.Groups.Select(p =>
{
IdentityReference ir = null;
try { ir = p.Translate(typeof(NTAccount)); }
catch { }
return ir == null ? null : ir.Value;
});
}
}
}
// Win32 API part
internal sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private SafeTokenHandle() : base(true) { }
[DllImport("kernel32.dll")]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr handle);
protected override bool ReleaseHandle()
{
return CloseHandle(handle);
}
}
internal sealed class ImpersonationException : Exception
{
public ImpersonationException() : base("The user is already impersonated.") { }
}
}
This works great for me. Check the user who is running the process. Sometime the user is not administrator or cannot impersonate.
Processinfo creates new process. Try process.start, Or you can convert exe to util dll and run inside code like utli.testuser code. Use dll call method from main program and not exe.
const int LOGON32_PROVIDER_DEFAULT = 0;
//This parameter causes LogonUser to create a primary token.
const int LOGON32_LOGON_INTERACTIVE = 2;
// Call LogonUser to obtain a handle to an access token.
SafeAccessTokenHandle safeAccessTokenHandle;
bool returnValue = LogonUser(userName, domainName, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out safeAccessTokenHandle);
WindowsIdentity.RunImpersonated(safeAccessTokenHandle, () => {
Var impersonatedUser = WindowsIdentity.GetCurrent().Name;
//--- Call your Method here…….
});
I am using windows functionality, impersonate, to change the active user before starting Chrome driver. Now, starting the driver without the impersonate code works fine. Also the impersonate code works fine; I see the user is changed. But when this change happens and after that I run IWebDriver driver=new ChromeDriver then the exception is triggered on that exact code and the test stops. Any ideas why this happens?
Here is main part of the code used (the code is just little modified code from another post here at stackoverflow)
namespace localSeleniumTest.Impersonation
{
class Program
{
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(stringpszUsername, string pszDomain, string pszPassword,
int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
// closes open handes returned by LogonUser
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public extern static bool CloseHandle(IntPtr handle);
public void Impersonation()
{
WindowsImpersonationContext impersonationContext = null;
IntPtr userHandle = IntPtr.Zero;
const int LOGON32_PROVIDER_DEFAULT = 0;
const int LOGON32_LOGON_INTERACTIVE = 2;
string domain = Config.domain;
string user = Config.username;
string password = Config.password;
try
{
String currentName = WindowsIdentity.GetCurrent().Name;
// if domain name was blank, assume local machine
if (domain == "")
domain = System.Environment.MachineName;
// Call LogonUser to get a token for the user
bool loggedOn = LogonUser(
user,
domain,
password,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
ref userHandle
);
if (!loggedOn)
{
return;
}
// Begin impersonating the user
impersonationContext = WindowsIdentity.Impersonate(userHandle);
String afterImpersonationName = WindowsIdentity.GetCurrent().Name;
/*this few lines below does not work after impersonation but
work perfectly without the code above.*/
IWebDriver driver = new ChromeDriver();
driver.Navigate().GoToUrl("www.google.com");
System.Threading.Thread.Sleep(6000);
driver.Quit();
Found the issue. D user did not have permission to chrome driver i Bin folder
We have a scenario in which we are trying to open CRM in Winform WebBrowser control and every time we open it, it asks for authentication in pop up window.
Also from that when we are trying to open a new form (which opens in IE) it asks for authentication again.
The catch here is that users are logged in into machines (Windows 7, IE 9) through local credentials and we can’t use integrated authentication.
We have captured user credentials in application i.e. we know user name and password, Is there a way through which we can pass the credentials to Webbrowser control and IE so that it does not ask user for credentials.
Appreciate You Help.
Thanks.
Abhinav
If you log in from the browser and authenticate yourself, you will see the following cookies being stored:
Theoretically, retaining these cookies across requests should prevent crm from asking for authentication each time you open a new instance of the WebBrowser Control.
You can set Cookies for the control using the WebBrowser.Document.Cookie property.
To retrieve the cookie after authenticating the first time, you will need to use the following code:
[SecurityCritical]
public static string GetCookieInternal(Uri uri, bool throwIfNoCookie)
{
uint pchCookieData = 0;
string url = UriToString(uri);
uint flag = (uint)NativeMethods.InternetFlags.INTERNET_COOKIE_HTTPONLY;
//Gets the size of the string builder
if (NativeMethods.InternetGetCookieEx(url, null, null, ref pchCookieData, flag, IntPtr.Zero))
{
pchCookieData++;
StringBuilder cookieData = new StringBuilder((int)pchCookieData);
//Read the cookie
if (NativeMethods.InternetGetCookieEx(url, null, cookieData, ref pchCookieData, flag, IntPtr.Zero))
{
DemandWebPermission(uri);
return cookieData.ToString();
}
}
int lastErrorCode = Marshal.GetLastWin32Error();
if (throwIfNoCookie || (lastErrorCode != (int)NativeMethods.ErrorFlags.ERROR_NO_MORE_ITEMS))
{
throw new Win32Exception(lastErrorCode);
}
return null;
}
private static void DemandWebPermission(Uri uri)
{
string uriString = UriToString(uri);
if (uri.IsFile)
{
string localPath = uri.LocalPath;
new FileIOPermission(FileIOPermissionAccess.Read, localPath).Demand();
}
else
{
new WebPermission(NetworkAccess.Connect, uriString).Demand();
}
}
private static string UriToString(Uri uri)
{
if (uri == null)
{
throw new ArgumentNullException("uri");
}
UriComponents components = (uri.IsAbsoluteUri ? UriComponents.AbsoluteUri : UriComponents.SerializationInfoString);
return new StringBuilder(uri.GetComponents(components, UriFormat.SafeUnescaped), 2083).ToString();
}
internal sealed class NativeMethods
{
#region enums
public enum ErrorFlags
{
ERROR_INSUFFICIENT_BUFFER = 122,
ERROR_INVALID_PARAMETER = 87,
ERROR_NO_MORE_ITEMS = 259
}
public enum InternetFlags
{
INTERNET_COOKIE_HTTPONLY = 8192, //Requires IE 8 or higher
INTERNET_COOKIE_THIRD_PARTY = 131072,
INTERNET_FLAG_RESTRICTED_ZONE = 16
}
#endregion
#region DLL Imports
[SuppressUnmanagedCodeSecurity, SecurityCritical, DllImport("wininet.dll", EntryPoint = "InternetGetCookieExW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]internal static extern bool InternetGetCookieEx([In] string Url, [In] string cookieName, [Out] StringBuilder cookieData, [In, Out] ref uint pchCookieData, uint flags, IntPtr reserved);
#endregion
}
I usually refrain from posting code I haven't tried out myself but I don't have access to an On-Prem environment at the moment. I would love to know if this works so let me know if you try it out.
I have code that creates an impersonation block to allow read access to a remote ini file, and write access to a remote directory. When the "remote" directory to write to is truly a remote computer UNC path, the system writes just fine, however if the "remote" ini file is truly a remote UNC path, GetPrivateProfileSectionNames returns 0. If, however, the "remote" ini file is really just a local UNC path, this function works as expected. Is there a way to get this function to work as expected for the case where the ini file is truly on a remote computer?
My impersonation block is done using the following disposable class:
[PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
public class Impersonation : IDisposable
{
private WindowsImpersonationContext _impersonatedUserContext;
// Declare signatures for Win32 LogonUser and CloseHandle APIs
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool LogonUser(
string principal,
string authority,
string password,
LogonSessionType logonType,
LogonProvider logonProvider,
out IntPtr token);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int DuplicateToken(IntPtr hToken,
int impersonationLevel,
ref IntPtr hNewToken);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool RevertToSelf();
// ReSharper disable UnusedMember.Local
enum LogonSessionType : uint
{
Interactive = 2,
Network,
Batch,
Service,
NetworkCleartext = 8,
NewCredentials
}
// ReSharper disable InconsistentNaming
enum LogonProvider : uint
{
Default = 0, // default for platform (use this!)
WinNT35, // sends smoke signals to authority
WinNT40, // uses NTLM
WinNT50 // negotiates Kerb or NTLM
}
// ReSharper restore InconsistentNaming
// ReSharper restore UnusedMember.Local
/// <summary>
/// Class to allow running a segment of code under a given user login context
/// </summary>
/// <param name="user">domain\user</param>
/// <param name="password">user's domain password</param>
public Impersonation(string user, string password)
{
var token = ValidateParametersAndGetFirstLoginToken(user, password);
var duplicateToken = IntPtr.Zero;
try
{
if (DuplicateToken(token, 2, ref duplicateToken) == 0)
{
throw new Exception("DuplicateToken call to reset permissions for this token failed");
}
var identityForLoggedOnUser = new WindowsIdentity(duplicateToken);
_impersonatedUserContext = identityForLoggedOnUser.Impersonate();
if (_impersonatedUserContext == null)
{
throw new Exception("WindowsIdentity.Impersonate() failed");
}
}
finally
{
if (token != IntPtr.Zero)
CloseHandle(token);
if (duplicateToken != IntPtr.Zero)
CloseHandle(duplicateToken);
}
}
private static IntPtr ValidateParametersAndGetFirstLoginToken(string user, string password)
{
if (string.IsNullOrEmpty(user))
{
throw new ConfigurationErrorsException("No user passed into impersonation class");
}
var userHaves = user.Split('\\');
if (userHaves.Length != 2)
{
throw new ConfigurationErrorsException("User must be formatted as follows: domain\\user");
}
if (!RevertToSelf())
{
throw new Exception("RevertToSelf call to remove any prior impersonations failed");
}
IntPtr token;
var result = LogonUser(userHaves[1], userHaves[0],
password,
LogonSessionType.Interactive,
LogonProvider.Default,
out token);
if (!result)
{
throw new ConfigurationErrorsException("Logon for user " + user + " failed.");
}
return token;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
// Stop impersonation and revert to the process identity
if (_impersonatedUserContext != null)
{
_impersonatedUserContext.Undo();
_impersonatedUserContext.Dispose();
_impersonatedUserContext = null;
}
}
}
While inside of an impersonation block instance of this class, the remote ini file is access by:
int bufLen = GetPrivateProfileSectionNames(buffer,
buffer.GetUpperBound(0),
iniFileName);
if (bufLen > 0)
{
//process results
}
How do I get GetPrivateProfileSectionNames to return valid data when dealing with a remote computer? Are there permissions my user needs on this or the remote computer?
At this point in time, I've not been able to find information about impersonation and how it interacts with win32 dlls/apis, however, I do know the following:
1) if the entire process is running under a user with access to the remote folder the ini file lives in, then GetPrivateProfileSectionNames works as desired
2) if GetPrivateProfileSectionNames is called inside an impersonation block, then it does not work as desired
3) if a file stream is opened, and the ini file is copied local, then GetPrivateProfileSectionNames is used on the local ini file, then GetPrivateProfileSectionNames works as desired, and the file stream is allowed access to the remote file.
I speculate, based on results, that the win32 api call GetPrivateProfileSectionNames is not getting passed the impersonation context from c#, and thus is running under the overall process context, which does not have access. I worked around this by caching the ini file local, and tracking when it was last changed so I know if the ini file needs to be re-cached, or if the local copy is correct.
I have a windows service that runs under the local machine system account. Inside this service, it tries to read a remote .ini file that is available on a remote shared folder. The code trying to read this file is wrapped in impersonation using LogonUser (a simplified version of the code is below for an idea of what it is doing). The impersonation successfully starts impersonating the user configured, however the instant it attempts to read the remote ini file found on the remote network share, an UnauthorizedAccessException is thrown. This happens even though the configured user has read/write permissions on the remote machine. When I modify the code to remove all impersonation, and instead run the windows service as the configured user, all access to the remote .ini file are successful. I would prefer to use impersonation to get access to this file rather than a hack such as running the service as the user. Can anyone see errors with the impersonation code in the example? Is there something I need to do on my Vista 64 bit box to enable this? Are there specific active directory permissions my IT co-workers need to enable?
private WindowsImpersonationContext _impersonatedUser;
private IntPtr _token;
// Declare signatures for Win32 LogonUser and CloseHandle APIs
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool LogonUser(
string principal,
string authority,
string password,
LogonSessionType logonType,
LogonProvider logonProvider,
out IntPtr token);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr handle);
enum LogonSessionType : uint
{
Interactive = 2,
Network,
Batch,
Service,
NetworkCleartext = 8,
NewCredentials
}
enum LogonProvider : uint
{
Default = 0, // default for platform (use this!)
WinNT35, // sends smoke signals to authority
WinNT40, // uses NTLM
WinNT50 // negotiates Kerb or NTLM
}
....
var result = LogonUser(exchangeUserId, exchangeDomain,
password,
LogonSessionType.Network,
LogonProvider.Default,
out _token);
var id = new WindowsIdentity(_token);
_impersonatedUser = id.Impersonate();
try
{
//Validate access to the file on the remote computer/share
File.GetAccessControl(remoteFileAvailableInSharedFolder);
}
catch (UnauthorizedAccessException unauthorized)
{
...
}
....
// Stop impersonation and revert to the process identity
if (_impersonatedUser != null)
{
_impersonatedUser.Undo();
_impersonatedUser = null;
}
if (_token != IntPtr.Zero)
{
CloseHandle(_token);
_token = IntPtr.Zero;
}
Doing further research I discovered the core issue to be caused by the impersonation code. I had to add the use of the api DuplicateToken and added usage of the api RevertToSelf too. On making these changes (see class below) the impersonation worked as well as the code did when the whole service was ran under my domain user. Hope the code helps others with this same issue.
public class Impersonation : IDisposable
{
private WindowsImpersonationContext _impersonatedUserContext;
// Declare signatures for Win32 LogonUser and CloseHandle APIs
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool LogonUser(
string principal,
string authority,
string password,
LogonSessionType logonType,
LogonProvider logonProvider,
out IntPtr token);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int DuplicateToken(IntPtr hToken,
int impersonationLevel,
ref IntPtr hNewToken);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool RevertToSelf();
// ReSharper disable UnusedMember.Local
enum LogonSessionType : uint
{
Interactive = 2,
Network,
Batch,
Service,
NetworkCleartext = 8,
NewCredentials
}
// ReSharper disable InconsistentNaming
enum LogonProvider : uint
{
Default = 0, // default for platform (use this!)
WinNT35, // sends smoke signals to authority
WinNT40, // uses NTLM
WinNT50 // negotiates Kerb or NTLM
}
// ReSharper restore InconsistentNaming
// ReSharper restore UnusedMember.Local
/// <summary>
/// Class to allow running a segment of code under a given user login context
/// </summary>
/// <param name="user">domain\user</param>
/// <param name="password">user's domain password</param>
public Impersonation(string user, string password)
{
var token = ValidateParametersAndGetFirstLoginToken(user, password);
var duplicateToken = IntPtr.Zero;
try
{
if (DuplicateToken(token, 2, ref duplicateToken) == 0)
{
throw new Exception("DuplicateToken call to reset permissions for this token failed");
}
var identityForLoggedOnUser = new WindowsIdentity(duplicateToken);
_impersonatedUserContext = identityForLoggedOnUser.Impersonate();
if (_impersonatedUserContext == null)
{
throw new Exception("WindowsIdentity.Impersonate() failed");
}
}
finally
{
if (token != IntPtr.Zero)
CloseHandle(token);
if (duplicateToken != IntPtr.Zero)
CloseHandle(duplicateToken);
}
}
private static IntPtr ValidateParametersAndGetFirstLoginToken(string user, string password)
{
if (string.IsNullOrEmpty(user))
{
throw new ConfigurationErrorsException("No user passed into impersonation class");
}
var userHaves = user.Split('\\');
if (userHaves.Length != 2)
{
throw new ConfigurationErrorsException("User must be formatted as follows: domain\\user");
}
if (!RevertToSelf())
{
throw new Exception("RevertToSelf call to remove any prior impersonations failed");
}
IntPtr token;
var result = LogonUser(userHaves[1], userHaves[0],
password,
LogonSessionType.Interactive,
LogonProvider.Default,
out token);
if (!result)
{
throw new ConfigurationErrorsException("Logon for user " + user + " failed.");
}
return token;
}
public void Dispose()
{
// Stop impersonation and revert to the process identity
if (_impersonatedUserContext != null)
{
_impersonatedUserContext.Undo();
_impersonatedUserContext = null;
}
}
}