I am doing an LDAP query with DirectoryEntry/DirectorySearcher to authenticate a user in Active Directory via a C# web app like so (the ConnectionString property is just equivalent to LDAP://server.domain):
internal bool AuthenticateUser(string username, string password)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return false;
var entry = new DirectoryEntry(this.ConnectionString, username, password);
var searcher = new DirectorySearcher { SearchRoot = entry, Filter = "(objectclass=user)" };
try
{
var result = searcher.FindOne();
return true; //connection to AD succeeded, authentication was successful
}
catch (DirectoryServicesCOMException)
{
return false; //impersonating the user failed
}
}
These queries are all hitting an SBS server which, when you create a new user, appears to use uppercase values for the pre-Windows 2000 (i.e. NetBIOS) name. So, if I add a new user called "Test User", the username might be "tuser" but the NetBIOS name it specifies is "TUser". When a user puts in a user/pass that hits this method, "tuser" fails to be authenticated whereas "TUser" succeeds.
My question is whether it is possible to modify this so usernames don't have to be case-sensitive?
The attribute definition in the schema defines which characters can be used in an attribute value for the attribute being defined. The matching rule(s) - also in the attribute definition in the schema - determine how attribute values are compared for equality, substring, ordering, and so forth. The matching rule(s) determine the "case-sensitivity" (although it's really not that simple) of a comparison of attributes.
Matching rules must be used by the server (and clients) when comparing attribute values.
For OpenLDAP there is a syntax to filter values in case-sensitive way.
Two short examples:
(&(ou:caseExactMatch:=cwm)(objectClass=person))
+ will match case-sensitive ou= value of 'cwm'
- will NOT match 'CWM', 'CwM' or 'Cwm'
(&(ou=cwm)(objectClass=person))
+ will match case-insensitive (by default) all ou= values like 'cwm', 'CWM', 'CwM', 'Cwm'
The syntax seems to be:
attr:matchingRule:=value
See "8.3.4 Matching rules" at:
https://access.redhat.com/documentation/en-US/Red_Hat_Directory_Server/8.2/html/Administration_Guide/Finding_Directory_Entries-LDAP_Search_Filters.html#using-matching-rules
Also there are several matchingRule types:
https://ldapwiki.com/wiki/MatchingRule
Related
I have simple service that gets user details using Novell.Directory.Ldap.NETStandard and F# (I can provide transcript for c# if that is necessary, but this part is very similar) and it looks like this:
use connection = new LdapConnection();
connection.Connect(credentials.host, LdapConnection.DefaultPort);
connection.Bind($"{credentials.domain}\{credentials.username}", credentials.password);
match connection.Connected with
| true ->
let schema = connection.FetchSchema((connection.GetSchemaDn()));
let filter = $"(SAMAccountName={credentials.username})"
let searcher = connection.Search(String.Empty, LdapConnection.ScopeBase, filter, null, false);
return (searcher |> Some, String.Empty)
| false ->
raise (Exception())
return (None, $"Cannot connect to domain {credentials.domain} with user {credentials.username}")
Now I cant find information about group that this user is assign to, normally when I use Directory.Service I just add:
directorySearcher.Filter <- sprintf "(SAMAccountName=%s)"credentials.username
To directory searcher and I can filter this information out (as Directory.Service is windows limited i can not use it in this project), but I can not find any information how to use it in Novell.Directory.Ldap.
You have to provide the required attributes (ie. memberOf in order to read user's group) as an array of strings instead of null when calling Search() :
let attrs = [| "SAMAccountName"; "memberOf"; |];
let searcher = connection.Search(searchbase, scope, filter, attrs, false);
You can also pass "*" to get all non-operational attributes.
I have three case like this
When the user first sign in I will validate their request session id to build the cookie
When the user is already authenticated (previously signedin), I will validate the sessionId from their claim.
However there is another 2 case that comes up
When the user is already authenticated (previously signedin), do a certain activity, that can potentially prompt a change in their session Id, now if I validate with the session Id from their claim it will be wrong so I have to detect if there is a change between the claimSessionId and the requestSessionId => validate their requestSessionId.
After the user signed in some request performed by the user won't send the requestSessionId hence the requestSessionId is empty, then in that case we will take the claimSessionId. This should be the last case though. How do you suggest we handle that as well?
var sessionToValidate = !UserPreviouslySignedIn()
? GetRequestSessionId()
: GetClaimSessionId();
await ValidateUserRequest(context, sessionToValidate);
How do I apply the check condition that a user may previously signed in but if sessionId != claimId then validate sessionid to this block of code and not having a bunch of if condition?
If I understand correctly, then my honest opinion, you're trying to do too much at once, and that will confuse other developers (us here at SO included) when they review or maintain your code. Keep it simple:
sessionToValidate = null;
if (!UserPreviouslySignedIn())
sessionToValidate = GetRequestSessionId();
else {
if (GetRequestSessionId() != GetClaimSessionId())
sessionToValidate = GetRequestSessionId();
else
sessionToValidate = GetClaimSessionId();
}
To simplify this and make it easier to follow; your most common result is the request session ID as demonstrated above, so default to that:
var sessionToValidate = GetRequestSessionId();
Then, since your final condition requires the user to be logged in and the request ID to match the claim ID, then chain that together in a single check:
if (UserPreviouslySignedIn() && sessionToValidate == GetClaimSessionId())
sessionToValidate = GetClaimSessionId();
This results in simpler code:
var sessionToValidate = GetRequestSessionId();
if (UserPreviouslySignedIn() && sessionToValidate == GetClaimSessionId())
sessionToValidate = GetClaimSessionId();
Per your original request, this can be implemented using the conditional assignment operator:
sessionToValidate = !UserPreviouslySignedIn() ? GetRequestSessionId() :
GetRequestSessionId() != GetClaimSessionId() ? GetRequestSessionId() :
GetClaimSessionId();
However, due to issues with readability, I wouldn't recommend it.
What is best way to get canonical path from DistinguishedName in active directory when retrieving all groups/users? is there any issue with following implementation
foreach (SearchResult entry in results)
{
var distinguishedName = entry.Properties["distinguishedName"][0].ToString();
entry.RefreshCache(new string[] { "canonicalName" });
var canonicalName = entry.Properties["canonicalName"][0].ToString();
}
You code will not work, as SearchResult does not contain RefreshCache method. The best way to convert DN to CN is to use DsCrackNames function.
You need to convert from DS_NAME_FORMAT.DS_FQDN_1779_NAME to DS_NAME_FORMAT.DS_CANONICAL_NAME.
Active Directory connection is not required to perform this conversion. You need to use DS_NAME_FLAGS.DS_NAME_FLAG_SYNTACTICAL_ONLY flag and then pass IntPtr.Zero as a connection handle
Be aware that foreign security principals cannot be converted using this function
However, if you already query DN, you can query CN as well. In your case the following code should work:
foreach (SearchResult entry in results)
{
var distinguishedName = entry.Properties["distinguishedName"][0].ToString();
var canonicalName = entry.Properties["canonicalName"][0].ToString();
}
If entry does not contain CN, you need to add it to DirectorySearcher as a requested attribute before performing AD query
I have two goals with the below code
1) Get a list of Users who below to a specific AD group
2) Get the email/lastname/first name of all the users that belong to that group
If there is a better way to accomplish both please let me know.
I'm able to get the full DN but I'm not sure how to get the remaining data from the full DN, or if there is a better way to pull this info please let me know. below is the code I'm using but it gets error:
The value provided for adsObject does not implement IADs.
when I tried to do a DirectorySearcher using the full DN.
HashSet<string> User_Collection = new HashSet<string>();
SearchResultCollection sResults = null;
DirectoryEntry dEntryhighlevel = new DirectoryEntry("LDAP://CN=Global_Users,OU=Astrix,OU=Clients,OU=Channel,DC=astro,DC=net");
foreach (object dn in dEntryhighlevel.Properties["member"])
{
DirectoryEntry dEntry = new DirectoryEntry(dn);
Console.WriteLine(dn);
DirectorySearcher dSearcher = new DirectorySearcher(dEntry);
//filter just user objects
dSearcher.SearchScope = SearchScope.Base;
//dSearcher.Filter = "(&(objectClass=user)(dn="+dn+")";
dSearcher.PageSize = 1000;
sResults = dSearcher.FindAll();
foreach (SearchResult sResult in sResults)
{
string Last_Name = sResult.Properties["sn"][0].ToString();
string First_Name = sResult.Properties["givenname"][0].ToString();
string Email_Address = sResult.Properties["mail"][0].ToString();
User_Collection.Add(Last_Name + "|" + First_Name + "|" + Email_Address);
}
Speed is important and yes I understand I'm not using HashSet as it's designed.
I always use the System.DirectoryServices.AccountManagement.
One of the first things you will see is this: "Connections speeds are increased by using the Fast Concurrent Bind (FSB) feature when available. Connection caching decreases the number of ports used."
with that being said I did not test your code against this for speed you will have to do that your self but this is Microsoft's new library.
Here is my code example:
// Create the context for the principal object.
PrincipalContext ctx = new PrincipalContext(ContextType.Domain,
"fabrikam",
"DC=fabrikam,DC=com");
// Create an in-memory user object to use as the query example.
GroupPrincipal u = new GroupPrincipal(ctx) {DisplayName = "Your Group Name Here"};
// Set properties on the user principal object.
// Create a PrincipalSearcher object to perform the search.
PrincipalSearcher ps = new PrincipalSearcher {QueryFilter = u};
// Tell the PrincipalSearcher what to search for.
// Run the query. The query locates users
// that match the supplied user principal object.
PrincipalSearchResult<Principal> results = ps.FindAll();
foreach (UserPrincipal principal in ((GroupPrincipal)results.FirstOrDefault()).Members)
{
string email = principal.EmailAddress;
string name = principal.Name;
string surname = principal.Surname;
}
It looks like you're walking the group membership of some group in AD...(guessing this off of the member reference above)
Anyway, you need to decide what sort of API you're looking for. The one you're using now is a bit lower level (though you could go lower if you want :)). Going higher level is an option as the previous answer eludes to.
To shake out the code a bit more (and help with perf as you mentioned that it is impt to you):
Use the same connection you used for the group membership search itself (ie no additional connect/bind)
Do a base search where the base DN is the user DN, the search filter is (objectclass=*) and the attributes are ONLY the attributes you care about (no *)
You can remove page size. Paging is a way to ask for many objects in groups (aka pages) but a base search only returns 1 object so it doesn't actually do anything.
Base search result count should always be 1.
Keep in mind cross-domain issues too. Make sure you test your code with a 2 domain forest where a group of type domain local in domain1 has a member in it from domain 2. That will yield some additional work to get more properties as you need to connect to a DC in the other domain (or a GC if the few properties you care about are all in the GC partial attribute set...)
Also keep in mind security. If you don't have access to these properties for some user in your domain, what does the code do? The code above would fail in a nasty way. :) You might want to handle this more gracefully...
Hope this helps.
~Eric
I'm stumped..
I'm trying to get the userPrincipalName from AD as follows:
DirectorySearcher search = new DirectorySearcher("LDAP://DCHS");
search.Filter = String.Format("(SAMAccountName={0})", UserName);
SearchResult result = search.FindOne();
DirectoryEntry entry = result.GetDirectoryEntry();
_UPN = entry.Properties["userPrincipalName"][0].ToString();
But this gives me:
Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
Can anyone tell me why this is happening?
EDIT:
This code gets the SSID of the current user. I need to make this work on any user I enter into a text box.
WindowsIdentity windowsId = new WindowsIdentity(WindowsIdentity.GetCurrent().Token);
_SSID = windowsId.User.ToString()
I believe the issue is because you are treating the userPrincipalName entry as an array of values. Try modifying your code as follows:
DirectorySearcher search = new DirectorySearcher("LDAP://DCHS");
search.Filter = String.Format("(SAMAccountName={0})", UserName);
SearchResult result = search.FindOne();
DirectoryEntry entry = result.GetDirectoryEntry();
_UPN = entry.Properties["userPrincipalName"].Value.ToString();
Notice that I changed the last line from [0] to Value. That should fix your issue.
The one thing I would say is that I would do some checking before trying to read this value. There are cases where a user wouldn't have a UPN. In that case, the code would throw an error when you tried to access the field (the field wouldn't exist so it wouldn't be that you just need to make sure it isn't null).
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
Basically, you can define a domain context and easily find users and/or groups in AD:
// set up domain context
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
// find user by name
UserPrincipal user = UserPrincipal.FindByIdentity(UserName);
if(user != null)
{
string upn = user.UserPrincipalName;
}
The new S.DS.AM makes it really easy to play around with users and groups in AD:
The obvious thing to do to avoid the exception (if that's valid) is to do
if (entry.Properties["userPrincipalName"].Count > 0)
{
_UPN = entry.Properties["userPrincipalName"][0].ToString();
}
but if you were supposed to get a valid result and you aren't then I would check the LDAP connection string and such. There are a few LDAP browsers that you could use (commercial + trial) to get your connection string right.