I have a .Net application that reads user from active directory that is in a specific OU (ABCUsers). The following is the code:
string DomainIP = "some domain IP";
string ContainerConnectionString = "OU=ABCUsers,DC=test,DC=com";
PrincipalContext domain = new PrincipalContext(ContextType.Domain, DomainIP, ContainerConnectionString, ContextOptions.SimpleBind);
PrincipalSearcher searcher = new PrincipalSearcher();
UserPrincipal findUser = new UserPrincipal(domain);
findUser.SamAccountName = "some username";
searcher.QueryFilter = findUser;
UserPrincipal foundUser = (UserPrincipal)searcher.FindOne();
The above code works fine, but I need to change the code so that it retrieves a user whether he/she is in OU=ABCUsers or OU=XYZUsers but not in any other OU.
(update: reading it again)
function/loop solution
(I would nevertheless prefer the solution with the Global Catalog below, because it is much less code and more robust.)
Since it would probably not work with an OR-LDAP-search string when not using the Global Catalog as explained below, you could just kind of repeat the above (I guess working) code for the two OUs similar to this when put e.g. in a separate function (pseudo code):
UserPrincipal findUserInOu( String ou ) {
string DomainIP = "some domain IP";
string ContainerConnectionString = "OU=" + ou + ",DC=test,DC=com";
// ... above code continued
}
UserPrincipal foundUser = findUserInOu("ABCUsers");
if ( foundUser == null )
foundUser = findUserInOu("XYZUsers");
GlobalCatalog solution
As I said here, to do it with some OR-search string etc. did not work for me and it seems, you may have to use the Global Catalog service (on the default port 3268, if you have a MS Active Directory otherwise I don't know if other directory services would have this feature).
I guess you would have to specify this on the PrincipalContext which may use some other default (389?).
Related
How can I get a list of users from active directory? Is there a way to pull username, firstname, lastname? I saw a similar post where this was used:
PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "YOURDOMAIN");
I have never done anything with active directory so I am completely lost. Any help would be greatly appreciated!
If you are new to Active Directory, I suggest you should understand how Active Directory stores data first.
Active Directory is actually a LDAP server. Objects stored in LDAP server are stored hierarchically. It's very similar to you store your files in your file system. That's why it got the name Directory server and Active Directory
The containers and objects on Active Directory can be specified by a distinguished name. The distinguished name is like this CN=SomeName,CN=SomeDirectory,DC=yourdomain,DC=com. Like a traditional relational database, you can run query against a LDAP server. It's called LDAP query.
There are a number of ways to run a LDAP query in .NET. You can use DirectorySearcher from System.DirectoryServices or SearchRequest from System.DirectoryServices.Protocol.
For your question, since you are asking to find user principal object specifically, I think the most intuitive way is to use PrincipalSearcher from System.DirectoryServices.AccountManagement. You can easily find a lot of different examples from google. Here is a sample that is doing exactly what you are asking for.
using (var context = new PrincipalContext(ContextType.Domain, "yourdomain.com"))
{
using (var searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
foreach (var result in searcher.FindAll())
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
Console.WriteLine("First Name: " + de.Properties["givenName"].Value);
Console.WriteLine("Last Name : " + de.Properties["sn"].Value);
Console.WriteLine("SAM account name : " + de.Properties["samAccountName"].Value);
Console.WriteLine("User principal name: " + de.Properties["userPrincipalName"].Value);
Console.WriteLine();
}
}
}
Console.ReadLine();
Note that on the AD user object, there are a number of attributes. In particular, givenName will give you the First Name and sn will give you the Last Name. About the user name. I think you meant the user logon name. Note that there are two logon names on AD user object. One is samAccountName, which is also known as pre-Windows 2000 user logon name. userPrincipalName is generally used after Windows 2000.
If you want to filter y active accounts add this to Harvey's code:
UserPrincipal userPrin = new UserPrincipal(context);
userPrin.Enabled = true;
after the first using. Then add
searcher.QueryFilter = userPrin;
before the find all. And that should get you the active ones.
PrincipalContext for browsing the AD is ridiculously slow (only use it for .ValidateCredentials, see below), use DirectoryEntry instead and .PropertiesToLoad() so you only pay for what you need.
Filters and syntax here:
https://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx
Attributes here:
https://learn.microsoft.com/en-us/windows/win32/adschema/attributes-all
using (var root = new DirectoryEntry($"LDAP://{Domain}"))
{
using (var searcher = new DirectorySearcher(root))
{
// looking for a specific user
searcher.Filter = $"(&(objectCategory=person)(objectClass=user)(sAMAccountName={username}))";
// I only care about what groups the user is a memberOf
searcher.PropertiesToLoad.Add("memberOf");
// FYI, non-null results means the user was found
var results = searcher.FindOne();
var properties = results?.Properties;
if (properties?.Contains("memberOf") == true)
{
// ... iterate over all the groups the user is a member of
}
}
}
Clean, simple, fast. No magic, no half-documented calls to .RefreshCache to grab the tokenGroups or to .Bind or .NativeObject in a try/catch to validate credentials.
For authenticating the user:
using (var context = new PrincipalContext(ContextType.Domain))
{
return context.ValidateCredentials(username, password);
}
Certainly the credit goes to #Harvey Kwok here, but I just wanted to add this example because in my case I wanted to get an actual List of UserPrincipals. It's probably more efficient to filter this query upfront, but in my small environment, it's just easier to pull everything and then filter as needed later from my list.
Depending on what you need, you may not need to cast to DirectoryEntry, but some properties are not available from UserPrincipal.
using (var searcher = new PrincipalSearcher(new UserPrincipal(new PrincipalContext(ContextType.Domain, Environment.UserDomainName))))
{
List<UserPrincipal> users = searcher.FindAll().Select(u => (UserPrincipal)u).ToList();
foreach(var u in users)
{
DirectoryEntry d = (DirectoryEntry)u.GetUnderlyingObject();
Console.WriteLine(d.Properties["GivenName"]?.Value?.ToString() + d.Properties["sn"]?.Value?.ToString());
}
}
Include the System.DirectoryServices.dll, then use the code below:
DirectoryEntry directoryEntry = new DirectoryEntry("WinNT://" + Environment.MachineName);
string userNames="Users: ";
foreach (DirectoryEntry child in directoryEntry.Children)
{
if (child.SchemaClassName == "User")
{
userNames += child.Name + Environment.NewLine ;
}
}
MessageBox.Show(userNames);
I've not done any LDAP-based authentication before and also I've not worked with any LDAP server before. So I need a free online LDAP server to play with, I've found this https://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/
However my code is not working (or the info there has become invalid, I'm not sure), the result of authen is always false, here is my code:
path = "ldap.forumsys.com:389/dc=example,dc=com";
using (var pc = new PrincipalContext(ContextType.Domain, null, path))
{
//this always returns false
var ok = pc.ValidateCredentials("read-only-admin", "password");
}
Could you make it work on your side? Or at least please assert that the info there is invalid, in that case if possible please give me some other info (from other free LDAP servers for testing).
I don't think the server is Active Directory. You can refer to this question for how to connect to a LDAP server in C#.
Second Edit:
Checked with MS people. They also suggest LdapConnection.
https://github.com/dotnet/corefx/issues/31809
Edit:
I can use DirectoryEntry to bind to the server. I am not sure why PrincipalContext does not work, but you can try this way.
Here is a sample code for validating user and password.
Tested on .Net Core 2.1, with System.DirectoryServices package 4.5.0.
using System;
using System.DirectoryServices;
namespace LDAPTest
{
class Program
{
static void Main(string[] args)
{
string ldapServer = "LDAP://ldap.forumsys.com:389/dc=example,dc=com";
string userName = "cn=read-only-admin,dc=example,dc=com";
string password = "password";
var directoryEntry = new DirectoryEntry(ldapServer, userName, password, AuthenticationTypes.ServerBind);
// Bind to server with admin. Real life should use a service user.
object obj = directoryEntry.NativeObject;
if (obj == null)
{
Console.WriteLine("Bind with admin failed!.");
Environment.Exit(1);
}
else
{
Console.WriteLine("Bind with admin succeeded!");
}
// Search for the user first.
DirectorySearcher searcher = new DirectorySearcher(directoryEntry);
searcher.Filter = "(uid=riemann)";
searcher.PropertiesToLoad.Add("*");
SearchResult rc = searcher.FindOne();
// First we should handle user not found.
// To simplify, skip it and try to bind to the user.
DirectoryEntry validator = new DirectoryEntry(ldapServer, "uid=riemann,dc=example,dc=com", password, AuthenticationTypes.ServerBind);
if (validator.NativeObject.Equals(null))
{
Console.WriteLine("Cannot bind to user!");
}
else
{
Console.WriteLine("Bind with user succeeded!");
}
}
}
}
Reference:
https://www.c-sharpcorner.com/forums/ldap-authentication2
I figure it out too, and having no LDAP knowledge I´ve come up with this.
The problem in your solution may be first, you are using "ldap://" instead of "LDAP://", since it was something I came into when coding this. But I use System.DirectoryServices library.
I tested against this magnificent free to test LDAP server
var path = "LDAP://ldap.forumsys.com:389/dc=example,dc=com";
var user = $#"uid={username},dc=example,dc=com";
var pass = "password";
var directoryEntry = new DirectoryEntry(path, user, pass, AuthenticationTypes.None);
var searcher = new DirectorySearcher(directoryEntry);
searcher.PropertiesToLoad.Add("*");
var searchResult = searcher.FindOne();
I don´t understand exactly what all of this lines does, however, and lookign for a solution I found some recommendations.
on the path the "LDAP://" string should be on block mayus.
in the user, sometimes you need to use "cn=username-admin" for validating admins, be sure to also set Authentication type to ServerBind.
It seems as if read-only-admin is not a valid user. Try replacing:
var ok = pc.ValidateCredentials("read-only-admin", "password");
with
var ok = pc.ValidateCredentials("tesla", "password");
If that does not work, the other other issue would be on the LDAP's server side.
A good option regardless is to set up an Amazon Web Services EC2 server (it is free) and load Windows Server onto it. This gives you your own server and you learn how to set up an LDAP server (which is pretty easy).
To enable or disable a user on local computer, I am using the following snippet.
DirectoryEntry localMachine = new DirectoryEntry("WinNT://" + Environment.MachineName);
DirectoryEntry currentUser = localMachine.Children.Find(user, "Administrators");
currentUser.Invoke("AccountDisabled", new object[] { true });
currentUser.CommitChanges();
I am assigning user as a string. I am getting a "filepath not found error": Comexception Unhandled.
Is anything wrong with my code ?
If you're on .NET 3.5 and up, you should check out the System.DirectoryServices.AccountManagement (S.DS.AM) namespace. Read all about it here:
Managing Directory Security Principals in the .NET Framework 3.5
MSDN docs on System.DirectoryServices.AccountManagement
Basically, you can define a machine-level context and easily find users and/or groups in AD:
// set up domain context
PrincipalContext ctx = new PrincipalContext(ContextType.Machine);
// find a user
UserPrincipal user = UserPrincipal.FindByIdentity(ctx, "SomeUserName");
if(user != null)
{
user.Enabled = false;
user.Save();
}
The new S.DS.AM makes it really easy to play around with users and groups in AD!
According to the MSDN Page for DirectoryEntry:
Connect to a user on a computer. For example, "WinNT://<domain name>/<computer name>/<user name>". If you are connecting to a local computer, "WinNT://<computer name>/<user name>".
So, your code to grab the user should be:
DirectoryEntry currentUser = new DirectoryEntry("WinNT://" + Environment.MachineName + "/" + user);
use like this
DirectoryEntry local = new DirectoryEntry("WinNT://localhost");
DirectoryEntry user = local.Children.Find(username);
user.InvokeSet("AccountDisabled", true);
user.CommitChanges();
Try to use Uniform Naming Convention i.e. use "\\" like this:
DirectoryEntry localMachine = new DirectoryEntry("WinNT:\\" + Environment.MachineName);
How can I get a list of users from active directory? Is there a way to pull username, firstname, lastname? I saw a similar post where this was used:
PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "YOURDOMAIN");
I have never done anything with active directory so I am completely lost. Any help would be greatly appreciated!
If you are new to Active Directory, I suggest you should understand how Active Directory stores data first.
Active Directory is actually a LDAP server. Objects stored in LDAP server are stored hierarchically. It's very similar to you store your files in your file system. That's why it got the name Directory server and Active Directory
The containers and objects on Active Directory can be specified by a distinguished name. The distinguished name is like this CN=SomeName,CN=SomeDirectory,DC=yourdomain,DC=com. Like a traditional relational database, you can run query against a LDAP server. It's called LDAP query.
There are a number of ways to run a LDAP query in .NET. You can use DirectorySearcher from System.DirectoryServices or SearchRequest from System.DirectoryServices.Protocol.
For your question, since you are asking to find user principal object specifically, I think the most intuitive way is to use PrincipalSearcher from System.DirectoryServices.AccountManagement. You can easily find a lot of different examples from google. Here is a sample that is doing exactly what you are asking for.
using (var context = new PrincipalContext(ContextType.Domain, "yourdomain.com"))
{
using (var searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
foreach (var result in searcher.FindAll())
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
Console.WriteLine("First Name: " + de.Properties["givenName"].Value);
Console.WriteLine("Last Name : " + de.Properties["sn"].Value);
Console.WriteLine("SAM account name : " + de.Properties["samAccountName"].Value);
Console.WriteLine("User principal name: " + de.Properties["userPrincipalName"].Value);
Console.WriteLine();
}
}
}
Console.ReadLine();
Note that on the AD user object, there are a number of attributes. In particular, givenName will give you the First Name and sn will give you the Last Name. About the user name. I think you meant the user logon name. Note that there are two logon names on AD user object. One is samAccountName, which is also known as pre-Windows 2000 user logon name. userPrincipalName is generally used after Windows 2000.
If you want to filter y active accounts add this to Harvey's code:
UserPrincipal userPrin = new UserPrincipal(context);
userPrin.Enabled = true;
after the first using. Then add
searcher.QueryFilter = userPrin;
before the find all. And that should get you the active ones.
PrincipalContext for browsing the AD is ridiculously slow (only use it for .ValidateCredentials, see below), use DirectoryEntry instead and .PropertiesToLoad() so you only pay for what you need.
Filters and syntax here:
https://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx
Attributes here:
https://learn.microsoft.com/en-us/windows/win32/adschema/attributes-all
using (var root = new DirectoryEntry($"LDAP://{Domain}"))
{
using (var searcher = new DirectorySearcher(root))
{
// looking for a specific user
searcher.Filter = $"(&(objectCategory=person)(objectClass=user)(sAMAccountName={username}))";
// I only care about what groups the user is a memberOf
searcher.PropertiesToLoad.Add("memberOf");
// FYI, non-null results means the user was found
var results = searcher.FindOne();
var properties = results?.Properties;
if (properties?.Contains("memberOf") == true)
{
// ... iterate over all the groups the user is a member of
}
}
}
Clean, simple, fast. No magic, no half-documented calls to .RefreshCache to grab the tokenGroups or to .Bind or .NativeObject in a try/catch to validate credentials.
For authenticating the user:
using (var context = new PrincipalContext(ContextType.Domain))
{
return context.ValidateCredentials(username, password);
}
Certainly the credit goes to #Harvey Kwok here, but I just wanted to add this example because in my case I wanted to get an actual List of UserPrincipals. It's probably more efficient to filter this query upfront, but in my small environment, it's just easier to pull everything and then filter as needed later from my list.
Depending on what you need, you may not need to cast to DirectoryEntry, but some properties are not available from UserPrincipal.
using (var searcher = new PrincipalSearcher(new UserPrincipal(new PrincipalContext(ContextType.Domain, Environment.UserDomainName))))
{
List<UserPrincipal> users = searcher.FindAll().Select(u => (UserPrincipal)u).ToList();
foreach(var u in users)
{
DirectoryEntry d = (DirectoryEntry)u.GetUnderlyingObject();
Console.WriteLine(d.Properties["GivenName"]?.Value?.ToString() + d.Properties["sn"]?.Value?.ToString());
}
}
Include the System.DirectoryServices.dll, then use the code below:
DirectoryEntry directoryEntry = new DirectoryEntry("WinNT://" + Environment.MachineName);
string userNames="Users: ";
foreach (DirectoryEntry child in directoryEntry.Children)
{
if (child.SchemaClassName == "User")
{
userNames += child.Name + Environment.NewLine ;
}
}
MessageBox.Show(userNames);
What is the simplest and most efficient way in C# to check if a Windows user account name exists? This is in a domain environment.
Input: user name in [domain]/[user] format (e.g. "mycompany\bob")
Output: True if the user name exists, false if not.
I did find this article but the examples there are related to authenticating and manipulating user accounts, and they assume you already have a user distinguished name, whereas I am starting with the user account name.
I'm sure I can figure this out using AD, but before I do so I was wondering if there is a simple higher level API that does what I need.
* UPDATE *
There are probably many ways to do this, Russ posted one that could work but I couldn't figure out how to tweak it to work in my environment. I did find a different approach, using the WinNT provider that did the job for me:
public static bool UserInDomain(string username, string domain)
{
string path = String.Format("WinNT://{0}/{1},user", domain, username);
try
{
DirectoryEntry.Exists(path);
return true;
}
catch (Exception)
{
// For WinNT provider DirectoryEntry.Exists throws an exception
// instead of returning false so we need to trap it.
return false;
}
}
P.S.
For those who aren't familiar with the API used above: you need to add a reference to System.DirectoryServices to use it.
The link I found that helped me with this: How Can I Get User Information Using ADSI
The examples use ADSI but can be applied to .NET DirectoryServices as well. They also demonstrate other properties of the user object that may be useful.
The System.DirectoryServices namespace in the article is exactly what you need and intended for this purpose. If I recall correctly, it is a wrapper around the Active Directory Server Interfaces COM interfaces
EDIT:
Something like the following should do it (it could probably do with some checking and handling). It will use the domain of the current security context to find a domain controller, but this could easily be amended to pass in a named server.
public bool UserInDomain(string username, string domain)
{
string LDAPString = string.Empty;
string[] domainComponents = domain.Split('.');
StringBuilder builder = new StringBuilder();
for (int i = 0; i < domainComponents.Length; i++)
{
builder.AppendFormat(",dc={0}", domainComponents[i]);
}
if (builder.Length > 0)
LDAPString = builder.ToString(1, builder.Length - 1);
DirectoryEntry entry = new DirectoryEntry("LDAP://" + LDAPString);
DirectorySearcher searcher = new DirectorySearcher(entry);
searcher.Filter = "sAMAccountName=" + username;
SearchResult result = searcher.FindOne();
return result != null;
}
and tested with the following
Console.WriteLine(UserInDomain("username","MyDomain.com").ToString());
Found a simple way to do this if you're on a high enough framework version:
using System.DirectoryServices.AccountManagement;
bool UserExists(string userName, string domain) {
using (var pc = new PrincipalContext(ContextType.Domain, domain))
using (var p = Principal.FindByIdentity(pc, IdentityType.SamAccountName, userName)) {
return p != null;
}
}