I have a fairly odd requirement to be able to impersonate a user, when I'm already impersonating another, using C#.
I'm writing an app to allow the management of Active Directory users. This app will provide the ability for anyone in the company to view and maintain certain details about themselves (some of which will not actually be saved to Active Directory, but some of which will), for managers to be able to view and maintain details about their team, and for HR to be able to view and maintain details about anyone.
For obvious reasons I don't want to develop or test this against the live domain. We have recently ported all users over to this domain from another domain, which means I can actually test against the old domain without affecting anything. However, to enable me to do this I have to impersonate my old account on the old domain, which I do on loading the application.
Although for me everything will work fine as I'm setup as a domain admin, going forward obviously not all users will be domain admins, and won't be able to write to AD under their own account, and therefore we have another domain admin user setup specifically for this application, whenever data needs to be saved to AD that user is impersonated. This was working great before when I was testing against an Active Directory I'd setup on a virtual machine because I was logging onto the local domain, however that didn't allow me to step through the code in Visual Studio so debugging was slow, and hence I've stopped using that virtual machine and am using this old domain. Now I'm already impersonating another user (i.e. my old domain account), when it then tries to impersonate the domain admin user it fails with an "System.Security.SecurityException: Access is denied." exception. The line this fails on is just writing out some debugging information using "WindowsIdentity.GetCurrent().Name".
If I change my code so I'm actually logging in using the new domain admin rather than my old account, the first time it goes through it logs in successfully (so the credentials are correct), however when it then goes through and tries to do the same again to write to AD it fails with the above exception. Therefore I think it must be a problem with trying to do a nested impersonate.
Is it possible to do a nested impersonate?
Below is the code I'm using:
private static WindowsImpersonationContext ImpersonateUser(out string result, string sUsername,
string sDomain, string sPassword)
{
// initialize tokens
var pExistingTokenHandle = new IntPtr(0);
var pDuplicateTokenHandle = new IntPtr(0);
// if domain name was blank, assume local machine
if (sDomain == "")
{
sDomain = Environment.MachineName;
}
try
{
result = null;
const int logon32ProviderDefault = 0;
// create token
const int logon32LogonInteractive = 2;
// get handle to token
var bImpersonated = LogonUser(sUsername, sDomain, sPassword,
logon32LogonInteractive,
logon32ProviderDefault,
ref pExistingTokenHandle);
// did impersonation fail?
if (!bImpersonated)
{
var nErrorCode = Marshal.GetLastWin32Error();
result = "LogonUser() failed with error code: " + nErrorCode + "\r\n";
}
// Get identity before impersonation
result += string.Format("Before impersonation: {0}\r\n", WindowsIdentity.GetCurrent().Name);
var bRetVal = DuplicateToken(pExistingTokenHandle, (int)SecurityImpersonationLevel.SecurityImpersonation,
ref pDuplicateTokenHandle);
// did DuplicateToken fail?
if (bRetVal)
{
// create new identity using new primary token
var newId = new WindowsIdentity(pDuplicateTokenHandle);
var impersonatedUser = newId.Impersonate();
// check the identity after impersonation
result += "After impersonation: " + WindowsIdentity.GetCurrent().Name + "\r\n";
return impersonatedUser;
}
else
{
var nErrorCode = Marshal.GetLastWin32Error();
CloseHandle(pExistingTokenHandle); // close existing handle
result += "DuplicateToken() failed with error code: " + nErrorCode + "\r\n";
return null;
}
}
finally
{
// close handle(s)
if (pExistingTokenHandle != IntPtr.Zero)
{
CloseHandle(pExistingTokenHandle);
}
if (pDuplicateTokenHandle != IntPtr.Zero)
{
CloseHandle(pDuplicateTokenHandle);
}
}
}
When this is called for the nested impersonation which fails, "bImpersonated" is actually "true", as is bRetVal, which suggests its worked, however when it gets to "WindowsIdentity.GetCurrent().Name" it fails with the exception above.
I hope this makes sense, and would appreciate any assistance.
Related
I am trying to connect to Active Directory using service account credentials that have full access to connect to Active Directory, but unable to load property details of users.
This happens when I am logged in using 'miminstall' account which does not have access to fetch user details from AD, but in my app I have passed credentials of account that has access in AD.
When I run Visual Studio with different user (adma) that has full connection access to Active directory, I am able to connect and fetch user details without any issue.
I don't know why it is happening even though adma account credentials are passed in the code.
public string getADattributes(string DN, string operation)
{
string path = "LDAP://xyz.com";
DirectoryEntry directoryEntry = new DirectoryEntry(path, "xyz\\adma", "abc", AuthenticationTypes.Secure);
using (directoryEntry)
{
DirectorySearcher objDSearcher = new DirectorySearcher();
objDSearcher.Filter = "(distinguishedName=" + DN + ")";//search user in AD using DN
objDSearcher.PropertiesToLoad.Add("whenCreated");
objDSearcher.PropertiesToLoad.Add("whenChanged");
objDSearcher.PropertiesToLoad.Add("EmployeeID");
objDSearcher.SearchScope = SearchScope.Subtree;
SearchResult result = objDSearcher.FindOne();
if (result != null)//if count!=0 that means user exist in ad
{
string createdDate = "";
string modifiedDate = "";
string employeeID = "";
if (result.Properties["whenCreated"].Count >0)
{
//able to come inside if statement when running visual studio using adma account but not when runnning with login account i.e., miminstall
createdDate = result.Properties["whenCreated"][0].ToString();
}
if(result.Properties["whenChanged"].Count>0)
{
modifiedDate = result.Properties["whenChanged"][0].ToString();
}
if(result.Properties["EmployeeID"].Count > 0)
{
employeeID = result.Properties["EmployeeID"][0].ToString();
}
}
return null;
}
}
Unless this is a one time task, one would typically create a task in task scheduler or a webapp under IIS.
If this is a console application, add a new Task under Task Scheduler, set the action to run your app (give it path to your app's exe), and set the task user as 'adma'
If it's part of a webapp, create a new application pool in IIS. Then right click the newly created app pool, go to Advanced Settings > Identity and provide the credentials for 'adma'. Then assign this application pool to your webapp.
If this is not going to be a scheduled task or a webapp, and occasional on-demand run, I believe adding Impersonation would be your best option. See this SO
I have some code that was working when just using active directory,however, when publishing it gives an error.
//Allow Active Directory Credentials to remove someone from the global unsubscribe
protected void Remove_Click(object sender, EventArgs e)
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, "Domain"))
{
// find the group in question
GroupPrincipal group = GroupPrincipal.FindByIdentity(pc, "IT Group");
if (group != null)
{ // remove user}
This now crashes the webpage and i get the following error:
Runtime Error
Description: An application error occurred on the server. The current custom error settings for this application prevent the details of the application error from being viewed remotely (for security reasons). It could, however, be viewed by browsers running on the local server machine.
Update:
Principal user can't be used with ASP.NET forms instead you must use a claims form and check your claims/roles.
// Cast the Thread.CurrentPrincipal
ClaimsPrincipal icp = User as ClaimsPrincipal;
// Access IClaimsIdentity which contains claims
ClaimsIdentity claimsIdentity = (ClaimsIdentity)icp.Identity;
// what we are doing here is using a for each to get to the claim
foreach (Claim claim in claimsIdentity.Claims)
{
//The claim we are looking for is in this directory /groupsid
if (claim.Type == "http://schemas.microsoft.com/claims/groupsid")
{
//We use a for loop to go through this because it reads it like a JSON and will either pull the first or last
//Basic for loop incrementing until it finds our group (IT ALL)
for (int i = 0; i <= 0; i++)
{
//If our claim contains IT ALL (which it does) then do something...
if (claim.Value.Contains("Name of Group"))
{
//I'm storing the result here so we can call it otherwise we lose it...
lblResultq.Text = (claim.Value.ToString());
Hope this will help someone in the future!
I'm able to validate a user by forcing a bind on a DirectoryEntry object like so:
public bool ValidateUser(string username, string password)
{
using (var directoryEntry = new DirectoryEntry("LDAP:" + "//" + server +
"/" + containerDistinguishedName,
domain + #"\" + username, password,
AuthenticationTypes.Secure))
{
// invoke a bind to the directory entry
try
{
var obj = directoryEntry.NativeObject;
return true;
}
catch
{
return false;
}
}
}
This is similar to the previous way I was doing this here:
PrincipalContext.ValidateCredentials doesn't set lastLogon date for
user
However, I was unable to get that working and I eventually started decompiling some other code which led me to write this.
When a user is validated with this code, the lastLogon Active Directory attribute is not set regardless of how many successful validations they go through. One strange feature (which I have specified in the title of this question) is that two failed attempts and then one successful login will update the lastLogon value in the directory.
If the user fails to login once but then succeeds on the second attempt, the value isn't updated. It's only specifically after two failures that their lastLogon attribute updates.
Does anybody know why this is?
Firstly, I am new to AD / LDAP etc so this might be something obvious (I do hope so?!) and apologise if my terminology is not right, please correct me.
We have two domains, BUSINESS (being a Global Group) and AUTH (being a Domain Local Group) with one way trust between them (AUTH trusts BUSINESS).
The following code works on MachineA sitting on BUSINESS domain when string LDAPServer = "BUSINESS".
But when run on MachineB sitting on AUTH domain with string LDAPServer = "AUTH", it shows the message 2f. User Not Found as the user returned in Step 2 is NULL. If I change string LDAPServer = "BUSINESS" then an exception is thrown in Step 1 that the domain controller cannot be found.
As a note the fact that LDAPServiceAccount can be a BUSINESS user shows that AUTH can see BUSINESS. If I change this to LDAPServiceAccount = "BUSINESS\\NotRealName" then Step 1 throws an exception with invalid credentials. This suggests it has resolved the BUSINESS domain user to authenticate the call? If I change LDAPServiceAccount = "AUTH\\ValidAccount" I get the same issue of User == NULL and so 2f. User Not Found.
namespace ConsoleApplication1
{
using System;
using System.DirectoryServices.AccountManagement;
class Program
{
static void Main(string[] args)
{
// Who are we looking for?
string userName = "BUSINESS\\User.Name";
// Where are we looking?
string LDAPServer = "AUTH";
string LDAPServiceAccount = "BUSINESS\\InternalServiceAccountName";
string LDAPServiceAccountPassword = "CorrespondingPassword";
Console.WriteLine("1. Connecting to: " + LDAPServer);
using (PrincipalContext adPrincipalContext = new PrincipalContext(ContextType.Domain, LDAPServer, LDAPServiceAccount, LDAPServiceAccountPassword))
{
Console.WriteLine("2. Finding: " + userName);
using (UserPrincipal user = UserPrincipal.FindByIdentity(adPrincipalContext, userName))
{
if (user == null)
{
Console.WriteLine("2f. User Not Found!");
Console.ReadKey();
return;
}
Console.WriteLine("3. Getting groups...");
using (var groups = user.GetGroups())
{
Console.WriteLine("4. The groups are:");
foreach (Principal group in groups)
{
Console.WriteLine("\t{0}", group.Name);
}
}
}
}
Console.WriteLine("END.");
}
}
}
I am wondering if this is an issue between the two AD Servers where the AUTH is not checking with BUSINESS but checking if the user exists on its configuration? Do I need to setup my LDAPServiceAccount user to have any special permissions? Any pointers would be much appreciated!
I guess you need to check pre-conditions while communicating with the AD server
(1) Your machine should be on the same domain to which you are communicating.
(2) Username and Password which you passed in query should exist on that AD server.
(3) In case you are updating records, then the credential you have passed to the server should have Admin rights.
Please check these points once.
Hope this helps you.
Prerequisite Detail
Working in .NET 2.0.
The code is in a common library that could be called from ASP.Net, Windows Forms, or a Console application.
Running in a Windows Domain on a corporate network.
The Question
What is the best way to get the current user's SID? I'm not talking about the identity that is executing the application, but the user who is accessing the interface. In background applications and desktop based applications this should be the identity actually executing the application, but in ASP.Net (without impersionation) this should be the HttpContext.Current.User SID.
The Current Method
This is what I've got right now. It just seems... wrong. It's nasty. Is there a better way to do this, or some built in class that does it for you?
public static SecurityIdentifier SID
{
get
{
WindowsIdentity identity = null;
if (HttpContext.Current == null)
{
identity = WindowsIdentity.GetCurrent();
}
else
{
identity = HttpContext.Current.User.Identity as WindowsIdentity;
}
return identity.User;
}
}
I don't think there is a better way at getting at this info--you've got to get at the WindowsPrincipal somehow and .NET's rather clean API abstracts that behind the User object. I'd just leave this nice and wrapped up in a method and call it a day.
Well, ok, there is one thing you should do (unless your web users are always going to be WindowsIdentity), which would be to check for null identity and handle it according to whatever rules you have.
Without the use of third-party libraries This code will give correct results, if the user changed his user name.
String SID = "";
string currentUserName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.ToString();
RegistryKey regDir = Registry.LocalMachine;
using (RegistryKey regKey = regDir.OpenSubKey(#"SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\SessionData", true))
{
if (regKey != null)
{
string[] valueNames = regKey.GetSubKeyNames();
for (int i = 0; i < valueNames.Length; i++)
{
using (RegistryKey key = regKey.OpenSubKey(valueNames[i], true))
{
string[] names = key.GetValueNames();
for (int e = 0; e < names.Length; e++)
{
if (names[e] == "LoggedOnSAMUser")
{
if (key.GetValue(names[e]).ToString() == currentUserName)
SID = key.GetValue("LoggedOnUserSID").ToString();
}
}
}
}
}
}MessageBox.Show(SID);
The WindowsIdentity class is the "built in class that does it for you". You've got as simple a solution as you're going to get really, as long as you have a valid WindowsIdentity to work with.
Alternatively, if you have the username of the user in question and you want to get the SID directly from AD, you can build your own library to use the DirectoryServices namespace and retrieve the DirectoryEntry for your user (this is a fairly complicated process as DirectoryServices is tricky). You can even use LDAP to get it if you have a need.
//the current user
WindowsIdentity.GetCurrent().Owner
Returns the security identifier (SID) in SDDL format
you know them as S-1-5-9.
WindowsIdentity.GetCurrent().Owner.ToString()
Returns the account domain security identifier (SID)
If the SID does not represent a Windows account SID, this
property returns null
WindowsIdentity.GetCurrent().Owner.AccountDomainSid
You could use this as:
_pipeSecurity = new PipeSecurity();
var sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, WindowsIdentity.GetCurrent().Owner);
var audid = new PipeAuditRule(sid, PipeAccessRights.FullControl, AuditFlags.Failure | AuditFlags.Success);
var access = new PipeAccessRule(sid, PipeAccessRights.ReadWrite, AccessControlType.Allow);
_pipeSecurity.AddAuditRule(audid);
_pipeSecurity.AddAccessRule(access);