I have the following block of code that queries Active Directory for users by Group Name using System.DirectoryServices.AccountManagement:
var domainContext = new PrincipalContext(ContextType.Domain, "company.container.internal");
var groupPrincipal = GroupPrincipal.FindByIdentity(domainContext, IdentityType.Name, "Lvl1Users");
if (groupPrincipal != null)
{
//Read the values
}
Now the site uses the following:
IIS7 on Win2k8
Windows Authentication
Impersonation = True
App Pool on .NET 4.0 using 'NETWORK SERVICE' as the account
On my local machine (you know how this goes) it all works great. My peers that try it locally also it works well. However once deployed to the server it shows the following:
An operations error occurred.
Everything I research says it's a permissions issue. 1 thing to note, on my local machine I'm on the MainNetwork domain which is the parent to company.container.internal domain which I am querying. The IIS machine is on company.container.internal and is querying the same domain. So honestly, I would think the more challenging situation is reading AD on my local machine which is on a different domain, but it works. On the server which is querying the same domain, it fails.
Here is what I've tried, and none of these has worked:
Change AppPool to 'LocalSystem'
Change AppPool to use a static super-duper Admin account
Used Impersonation in code to manipulate the context of the calls in a local block with an admin user on the MainNetwork domain.
Used Impersonation in code to manipulate the context of the calls in a local block with an admin user on the company.container.internal domain.
Adding in using (HostingEnvironment.Impersonate())
What gives here? I have tried impersonating every type of power admin on both domains, and used multiple AppPool settings, and I keep getting the same error. Is there anything that needs to change in the code with the declaration of the domains, or is there a permissions issue I'm missing?
I figured this out and it turned out that using HostingEnvironment.Impersonate() was still at the root to solve the problem. I had already tried this, but there was another issue with my code.
The issue is often that the context for which the Active Directory calls is made is under a user that does not have permissions (also can happen when identity impersonate="true" in ASP.NET, due to the fact that the users token is a "secondary token" that cannot be used when authenticating against another server from: http://bit.ly/1753RjA).
The following code will ensures that the block of code running, is run under the context of say the AppPool (i.e. NETWORKSERVICE) that the ASP.NET site is running under.
using (HostingEnvironment.Impersonate())
{
var domainContext = new PrincipalContext(ContextType.Domain, "myDomain.com");
var groupPrincipal = GroupPrincipal.FindByIdentity(domainContext, IdentityType.Name, "PowerUsers");
if (groupPrincipal != null)
{
//code to get the infomation
}
}
However, one super important detail is that all the code calling Active Directory must be in that block. I had used some code a team member of mine wrote that was returning a LINQ query results of type Users (custom class), but not evaluating the expression (bad practice). Therefore the expression tree was returned instead of the results.
What ended up happening is the calling code eventually evaluated the results and the An operations error occurred message still appeared. I though the code fix above didn't work. When in fact it did, but there was code evaluating the results outside the block.
In a nutshell, make sure all code to access Active Directory is inside that using block and the exception should be fixed one the service/app is deployed to the server.
Related
I'm having this issue where I'm trying to check if NT\Authority Local Service has read\execute permissions on a directory (folder). The product that I work on REQUIRES that the folder the user is installing to has read\execute permissions set for Local Service.
The problem is that when I get the Access Control List (ACL) recursively (groups-within-groups), Local Service is not listed so I can't check if he has permissions to that folder or not.
By default, Local Service does not have read/execute permissions to user profiles (My Documents, Desktop, etc...) but I won't know if Local Service has access to other directories the user chooses to install to.
NOTE: Local Service DOES have access to Program Files, even though it is NOT listed in the ACL. Is it hidden somewhere else?
This is a short snippet on how I'm pulling the ACL:
GroupPrincipal groupPrincipal =
GroupPrincipal.FindByIdentity(principalContext, identityReferenceValue);
// GetMembers(true) is recursive (groups-within-groups)
foreach (var member in groupPrincipal.GetMembers(true)) {
if (member.SamAccountName.Equals("LOCAL SERVICE")) {
foundLocalService = true;
break;
}
}
Is there any other way I should be doing this? (Other than adding an access rule for Local Service on that directory)
Is Local Service just not listed in Directories ACL's?
Any help would be greatly appreciated.
It's notoriously difficult to calculate "effective permissions" for an account. But the simple answer to your question is that you will likely want to look for either on of:
The local Users group, sometimes shown as BUILTIN\Users or COMPUTERNAME\Users, or
Authenticated Users, sometimes shown as NT AUTHORITY\Authenticated Users.
Authenticated Users is one of the well-know SIDs. It is "a group that includes all users whose identities were authenticated when they logged on.". As long as you can prove who you are, you are included in Authenticated Users. The SID for this is always S-1-5-11 on every Windows computer.
However, it's not really considered a real group. To find it when adding permissions to a folder, you have to have "Built-in security principals" selected under "Select this object type":
The local Users group contains Authenticated Users by default. On my computer, I actually see both Users and Authenticated Users in the default permissions on the file system.
That's what you will most likely see, and that's likely all that matters.
But that's not the only way. You could see Everyone (S-1-1-0), which includes every user, authenticated or not.
Or, it could be a file or folder that has the LOCAL SERVICE account as the owner.
Or, there could be a local group that was created manually and LOCAL SERVICE was added to.
One way to get a more authoritative list of what you can look for is to run this under the LOCAL SERVICE account:
whoami /groups
That will tell you every group in the authentication token, which is every group that you are considered a member of for authentication purposes.
But you can't just open a command prompt as LOCAL SERVICE. So one way to do this is to open the Task Scheduler and create a task that runs under LOCAL SERVICE, with the action of:
Program: cmd
Arguments: /c "whoami /groups > C:\temp\localservice.txt"
Then run the task and, when it's done, look at C:\temp\localservice.txt. It will have a table of group names and their SIDs that you can look for.
I have an intranet application that authenticates users against active directory. In general it works fine when testing locally(i.e. from my dev machine using VS 2017) or running it off of IIS in app server ("Browse *:80") until I try to access it using its URL from my local machine. Then whatever user id I use to get user's detail info, nothing is displayed.
Also, this app is to be accessible to users in certain groups so application checks logged in user's group membership.
Here is how I have it set up in IIS and different scenarios I tested with:
I set authentication to "Windows Authentication", disabled anonymous
authentication and enabled AS.NET Impersonation. This works fine when
running from app server using localhost but when trying to access
from my local machine and supplying user's user id to get his/her
detail info, no details are displayed (fails to get any info from
AD).
If I enable anonymous authentication and set it to "Application Pool Identity" (i.e. Network Services), it displays my custom
"Access Denied" page, presumably because this user is not part of
allowed group.
If I enable anonymous authentication and select "Specific User", supplying my credentials, then everything works fine, from app server or from my local machine, with one caveat: no matter who is accessing the site, it shows my name as logged in user.
I am stumped ad would appreciate some hints.
Update - Added code to get user info
Code to get user's identity:
WindowsIdentity wiUser = WindowsIdentity.GetCurrent();
string sID = wiUser.Name.ToUpper();
Code to get user's AD info:
static string adDomain = "www.xxx.yyy.zzz";
static string adContainer = "DC=www,DC=xxx,DC=yyy,DC=zzz";
public static DataTable getUserADInfoDT(string sSearchStr)
{
DataTable dtResults = new DataTable();
dtResults.Columns.Add("ID");
dtResults.Columns.Add("FirstName");
...
dtResults.Columns.Add("Zip");
string adDomain = string.Empty;
string adContainer = string.Empty;
// create domain context
PrincipalContext adPrincipalContext = new PrincipalContext(ContextType.Domain, adDomain, adContainer);
using (adPrincipalContext)
{
// define a "query-by-example" principal
UserPrincipal qbeUser = new UserPrincipal(adPrincipalContext);
qbeUser.SamAccountName = sSearchStr.Trim().ToUpper();
// create principal searcher passing in the QBE principal
PrincipalSearcher srch = new PrincipalSearcher(qbeUser);
PrincipalSearchResult<Principal> psr = srch.FindAll();
// find all matches
foreach (var found in psr)
{
DataRow dr = dtResults.NewRow();
UserPrincipal up = (UserPrincipal)found;
DirectoryEntry de = (DirectoryEntry)up.GetUnderlyingObject();
dr["ID"] = de.Properties["SAMAccountName"].Value.ToString().ToUpper();
if (de.Properties["givenName"].Value != null)
dr["FirstName"] = de.Properties["givenName"].Value.ToString();
...
if (de.Properties["postalCode"].Value != null)
dr["Zip"] = de.Properties["postalCode"].Value.ToString();
dtResults.Rows.Add(dr);
//de.Close();
}
return dtResults;
}
}
WindowsIdentity.GetCurrent() will get the user account that the current process is running under. If the application is running in IIS, that will usually be the user account that the app pool is running under - not the user that logged in to your application.
If you use IIS Express for debugging locally, then IIS Express is running under your user account, so you wouldn't see a difference. But on the server you will.
That said, if you have impersonation turned on and working properly, then WindowsIdentity.GetCurrent() should return the user that logged in - since impersonation means that your app is now pretending to be that person. So this might mean that impersonation isn't setup right. But maybe you don't even need it. I've never personally found a need to use impersonation.
To get the user that logged into your app, you should use HttpRequest.Current.User or possibly just HttpContext.User if your app is ASP.NET MVC.
And there is actually an easier way to get a DirectoryEntry object for the user, without having to search for the username. You can bind directly to the SID of the user, which is information you already have:
var user = new DirectoryEntry(
$"LDAP://<SID={((WindowsIdentity) HttpContext.User.Identity).User.Value}>");
If your server is not joined to the same domain as your users, then you might need to include the domain name:
var user = new DirectoryEntry(
$"LDAP://www.xxx.yyy.zzz/<SID={((WindowsIdentity) HttpContext.User.Identity).User.Value}>");
Something else to keep in mind: DirectoryEntry keeps a cache of all the Properties. When you access a property that it doesn't have in the cache already, it will go out to AD and ask for every attribute with a value. That's a lot of useless data going over the network if you only need 3 attributes. So you can tell it to specifically ask for only those 4 attributes before you use them:
user.RefreshCache(new[] {"SAMAccountName", "givenName", "postalCode"});
I talked about this, and other points, in an article I wrote: Active Directory: Better Performance
Update: To verify if IIS is actually enforcing authentication for a page, you can do this in PowerShell (update the URL to what you need - and this requires at least PowerShell 3):
try { (Invoke-WebRequest "http://example.com/the/page").StatusCode }
catch { $_.Exception.Response.StatusCode.Value__}
That will output the status code of the reply. You should see 401, since that is how IIS challenges the browser to send credentials. If you see 200, then Windows Authentication is not working for that page.
I do this in PowerShell, since Chrome's dev tools won't even show you the 401 challenge.
This morning I started noticing some problems with several of my programs regarding Active Directory read operations. I noticed that all those applications (client and server) use the System.DirectoryServices.AccountManagement.UserPrincipal class for those read operations, while the programs still running correctly use System.DirectoryServices.DirectorySearcher.
So in order to narrow the problem down, I built the following, very simple console application
class Program
{
static void Main(string[] args)
{
//this works great
Console.WriteLine($"Enviroment.Username:{Environment.UserName}");
//this works great
PrincipalContext pcFull = new PrincipalContext(ContextType.Domain, "my.company.de", "dc=my,dc=company,dc=de");
UserPrincipal upPrincipalContextFull = UserPrincipal.FindByIdentity(pcFull, Environment.UserName);
//this doesn't work at all
//Exception: “The specified directory service attribute or value does not exist”
PrincipalContext pc = new PrincipalContext(ContextType.Domain);
UserPrincipal upPrincipalContext = UserPrincipal.FindByIdentity(pc, Environment.UserName);
//this doesn't either, same exception
UserPrincipal upCurrent = UserPrincipal.Current;
Console.ReadKey();
}
}
As you can see in the comments, the two latter operations will fail on every Computer in the domain i tested it on, even though they worked perfectly for several years. The following Exception occurs when I call UserPrincipal.Currentor UserPrincipal.FindByIdentity(pc, Environment.UserName); without specifying the Container in the PrincipalContext:
System.Runtime.InteropServices.COMException: “The specified directory service attribute or value does not exist”
Here is what I know:
none of the applications that suddenly stopped working received an update within the last two weeks
all of those applications, the UserPrincipal.Current-Property and the UserPrincipal.FindByIdentity-Method worked perfectly yesterday
Workstations did not receive Windows or .Net updates in the last week
the phenomenon does not relate to a single workstation, user or OS, but occures for a lot of different users, on a lot of different machines running Windows 7 or 10.
Domain Controllers received updates a week ago. Apparently one of those updates has a known issue about LDAP queries: Due to a defect in WLDAP32.DLL, applications that perform LDAP referral chasing can consume too many dynamic TCP ports. It seems unlikely that this is the reason for the sudden failures because a) that patch was installed a week ago and the Problems only occurred today and b) the suggested Workarounds from Microsoft (restarting Services) don't have any effect
What might cause such a behavior "overnight"? If it really is related to a Windows update, other users will soon be experiencing this bug too!
I can obviously build Workarounds, so I don't have to use the failing methods and properties, but I still have to know why it stopped working in the first place.
Edit
To start with, it would be useful to understand the difference between
public PrincipalContext(ContextType contextType); and public PrincipalContext(ContextType contextType, string name, string container);. The PrincipalContext constructed without container still has to obtain that container somehow, doesn't it?
By default the PrincipalContext searches in the "OU=Computers"-Container.
This fails if the reading permission is not set for the Container and will throw a COM Exception.
I have the following code snippet to test a plain-text username/password against the AD, which works fine if I hit F5 in visual studio and try it via the WCFTestClient, but once I deploy to IIS and try the same function, it will never return true for ValidCredentials; is there something security wise that needs to be set for the Identity that the Application Pool runs under?
I tried setting the App Pool identity to my own account (domain admin) just to test if this was the problem, but that didn't help either, so I'm a bit lost as to how to fix this.
The site (Custom API) has anonymous access set up.
try
{
// create a "principal context" - e.g. your domain (could be machine, too)
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, DomainName))
{
// validate the credentials
if (pc.ValidateCredentials(UserName, Password))
{
IsValid = true;
break;
}
}
}
catch (Exception)
{
LoggingControler.LogWarning(null, "Unreachable Domain: " + Domain);
}
I've gone over things again, and it's all due to Rights in Windows. Network Service somehow doesn't have enough rights to execute PrincipalContext.ValidateCredentials. If I change the Application Pool Identity to the identity of a Domain Administrator, the code works.
If someone can tell me how to set up a Limited User account with the proper rights to execute PrincipalContext.ValidateCredentials, I can finish this.
Ok, I've finally found my own answer via https://stackoverflow.com/questions/5140377/query-activedirectory-sometimes-not-working-asp-net-c and https://elgg.leeds.ac.uk/webteam/weblog/15385.html
As I've discovered, "Network Service" application pool identity holds the key to this problem...
Adding the read rights didn't work; so there's still something else wrong.
Did you enable Windows Authentication in IIS
Apologies in advance as I haven't had much experience with directories before.
I have an ASP.net application, and I have to validate its users against an Active Directory Application Mode instance running on Server 2k3. I was previously attempting a connection with DirectoryEntry and catching the COMException if the user's credentials (userPrincipalName & password) were wrong, but I had a number of problems when trying to bind as users who weren't a member of any ADAM groups (which is a requirement).
I recently found the System.DirectoryServices.AccountManagement library, which seems a lot more promising, but although it works on my local machine, I'm having some troubles when testing this in our testbed environment. Chances are I'm simply misunderstanding how to use these objects correctly, as I wasn't able to find any great documentation on the matter. Currently I am creating a PrincipalContext with a Windows username and password, then calling the AuthenticateCredentials with the user's userPrincipalName and password. Here's a very short exert of what I'm doing:
using (var serviceContext = new PrincipalContext(
ContextType.ApplicationDirectory,
serverAddress,
rootContainer,
ContextOptions.Negotiate | ContextOptions.SecureSocketLayer,
serviceAccountUsername,
serviceAccountPassword)) {
bool credentialsValid = serviceContext.ValidateCredentials(userID, password, ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind)
}
If the user's credentials are valid, I then go on to perform other operations with that principal context. As I said, this works for both users with and without roles in my own environment, but not in our testbed environment. My old DirectoryEntry way of checking the user's credentials still works with the same configuration.
After a very long morning, I was able to figure out the problem!
The exception message I was receiving when calling ValidateCredentials was extremely vague. After installing Visual Studio 2008 in the test environment (which is on the other side of the country, mind you!), I was able to debug and retrieve the HRESULT of the error. After some very deep searching in to Google, I found some very vague comments about "SSL Warnings" being picked up as other exceptions, and that enabling "SCHANNEL logging" (which I'm very unfamiliar with!) might reveal some more insight. So, after switching that on in the registry and retrying the connection, I was presented with this:
The certificate received from the remote server does not contain the expected name. It is therefore not possible to determine whether we are connecting to the correct server. The server name we were expecting is ADAMServer. The SSL connection request has failed. The attached data contains the server certificate.
I found this rather strange, as the old method of connecting via SSL worked fine. In any case, my co-worker was able to spot the problem - the name on the SSL certificate that had been issued on the server was that of the DNS name ("adam2.net") and not the host name ("adamserver"). Although I'm told that's the norm, it just wasn't resolving the correct name when using PrincipalContext.
Long story short; re-issuing a certificate with the computer name and not the DNS name fixed the problem!