There are three main APIs .NET app can use to interact with LDAP service. These three APIs lives under the System.DirectoryServices
namespace or S.DS
for short. The purpose of this note is to orient my self when searching for AD or LDAP related problems in .NET (and .NET core).
Examples below are in Powershell for rapid testing. Transalting to C# or other CLR language shouldn't be an issue.
This suite of API provide low level access to the LDAP protocol. You would have to have knowledge of how the server is configured and the details of LDAP protocol.
Here's an example of LDAP Bind operation.
# Load S.DS.Protocols types from the assembly
Add-Type -Path "C:\path\to\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.DirectoryServices.Protocols.dll"
# Create network credential to be used in LdapConnection
$networkCredential = New-Object System.Net.NetworkCredential "CN=user,CN=container", "password"
# Create directory identifier
$identifier = New-Object System.DirectoryServices.Protocols.LdapDirectoryIdentifier "localhost", 389, true, false
# Use Auth type suitable for your directory
$AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic
# Create the connection
$ldapConnection = New-Object System.DirectoryServices.Protocols.LdapConnection $identifier, $networkCredential, $AuthType
# Perform Bind operation
$ldapConnection.Bind()
Up to this point, you will have a binded connection ready for further LDAP operations like search, add entry, remove entry, etc.
LDAP works in request response manner, similar to HTTP. You can futher use the BeginSendRequest()
and EndSendRequest()
method of LdapConnection
to send Requests. Requests are represented as one of the subclass of S.DS.Protocols.DirectoryRequest
. For example, you can use the S.DS.Protocols.SearchRequest
to perform directory search. Below is an example of how to use SearchRequest
# obtain LdapConection instance. See previous example.
$ldapConnection = ...
# Create search request
$SearchScope = [System.DirectoryServices.Protocols.SearchScope]::Subtree
$searchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest "cn=esales,cn=dev", "(objectClass=person)", $searchScope, @("uid", "cn", "sn", "createtimestamp", "pwdChangeTime", "pwdAccountLockedTime")
# Send search request
$searchResponse = $ldapConnection.SendRequest($searchRequest)
# Enumerate the result
$searchResponse.Entries.ForEach({ $_.DistinguishedName })
If what you need to do is as simple as performing search, having to craft all the intervening requests is quite tedious. Using an instance of S.DS.DirectorySearcher
one can perform search much simpler. Here's an example
# Load S.DS types from the assembly
Add-Type -Path "C:\path\to\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5\System.DirectoryServices.dll"
# Use Auth type suitable for your directory.
# Note that for DirectoryEntry constructor, authType parameter is of different type than on the LdapConnection constuctor
$AuthType = [System.DirectoryServices.AuthenticationTypes]::None
# Define your root entry.
# Your root entry should be the container of your application objects within the ldap server.
# DirectoryEntry wont attempt authentication at this point
$rootEntry = New-Object System.DirectoryServices.DirectoryEntry "LDAP://localhost/cn=dev", "userdn", "password", $AuthType
# Create a DirectorySearcher instance which scoped to the $rootEntry
$searcher = New-Object System.DirectoryServices.DirectorySearcher $rootEntry
# Prepare your query
# You can use the and (&) or or (|) conjuction
# You can also use asterisk to indicate wildcard
$query = "(&(objectClass=user)(objectCategory=person)(|(SAMAccountName=*appuser1*)(name=*appuser1*)(userPrincipal=*appuser1*)))"
# Set $query to be $searcher filter
$searcher.Filter = $query
# Run the query and fetch the result
$result = $searcher.FindAll()
# Iterate over result
foreach($res in $result) {
$res.Properties
}
Heads Up S.DS.AccountManagement
is not LDAP compliant. It works agains Microsoft SAMs
such as AD, AD-LDS, or Windows account store. Until I check against LDAP directory, I leave this
here for reference.
High level account management tasks like creating or removing accounts is more sophisticated than simple directory search operations. The S.DS.AccountManagement
namespace provides utility classes that can be use to perform account management operation with fewer operations.
The Account Management API works by managing Principal
s within PrincipalContext
. There are three built in principal types:
UserPrincipal
which represents user accountsComputerPrincipal
which represents computer hostsGroupPrincipal
which reprsents group
PrincipalContext
is distinguished into three types, each reprsented by on of the S.DS.AccountManagement.ContextType
enum:
ContextType.ApplicationDirectory
represents AD-LDS directoriesContextType.Domain
represents AD directoriesContextType.Machine
represents Windows host
Here's a directory search task example. Here we search for an user object using query by example method.
# My objects are stored in AD-LDS so I'm using ApplicationDirectory context type
$contextType = [System.DirectoryServices.AccountManagement.ContextType]::ApplicationDirectory
# Context options. I use simple bind here.
$contextOptions = [System.DirectoryServices.AccountManagement.ContextOptions]::SimpleBind
# If needed, Context options can be bitwise or'd
$contextOptions = ([System.DirectoryServices.AccountManagement]::SimpleBind -bor [System.DirectoryServices.AccountManagement]::ServerBind)
# This is the Principal Context, where search will be rooted
$principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext $contextType, "localhost", "cn=dev"
# And this is the exemplary object. A User Principal.
# This is still a 'detached' object in that, it's attributes do not come from directory entry
$principal = New-Object System.DirectoryServices.AccountManagement.UserPrincipal $principalContext, "appuser1", "appuser1", true
# Create a PrincipalSearcher instance
$principalSearcher = New-Object System.DirectoryServices.AccountManagement.PrincipalSearcher $principal
# Perform the query and collect the results
$result = $principalSearcher.FindAll()
# Iterate over result
foreach($res in $result) {
$res.Properties
}
Q: Where are S.DS assemblies located on my server?
A: %WINDIR%\Microsoft.NET\assembly\GAC_MSIL
. On older version of .NET Fx %WINDIR%\assembly.
Q: How to convert LDAP timestamp to human readable date time and vice versa.
A: LDAP uses 18 digit timestamp for storage. To convert this timestamp to human readable time, you can use this converter