I've implemented Windows Auhtentication login to my .NET 7 application, but I'm stuck on resolving groups.
Sometimes, some of elements of the result of UserPrincipal.GetAuthorizationGroups is just null, but count of the result UserPrincipal.GetAuthorizationGroups is always the same.
I've tried to debug this issue and I've noticed that executing UserPrincipal.GetAuthorizationGroups().ToList() in the watch on breakpoint set on the Debug.WriteLine in the code below in Visual Studio gets all groups.
Like there is some delay to get data from Active Directory and I'm receiving not complete results.
My current code.
using (var pc = new PrincipalContext(ContextType.Domain))
{
var up = UserPrincipal.FindByIdentity(pc, "domainUserName");
var authorizationGroups = up.GetAuthorizationGroups();
foreach (var group in authorizationGroups)
{
if (group.Name == null)
{
Debug.WriteLine(group.Name);
}
claims.Add(
new Claim(ClaimTypes.Role, group.Name)
);
}
}
Tried changing
var authorizationGroups = up.GetAuthorizationGroups();
to
var authorizationGroups = up.GetAuthorizationGroups().ToList();
With no change.
I've noticed that calling up.GetGroups() instead of up.GetAuthorizationGroups() seems to works fine, at least dozens of times by logging in.
My question is what is to use UserPrincipal.GetAuthorizationGroups method and what is the reason of some null groups in the result.
The documentation for GetAuthorizationGroups() says:
This function only returns groups that are security groups; distribution groups are not returned.
And that it returns
null if the user does not belong to any groups.
GetGroups() will return both security groups and distribution groups. So if the user is a member of only distribution groups and not security groups, then GetGroups() will return results and GetAuthorizationGroups() will not.
Related
I simply want to get back the data where the AssignedToLName is equal to the currently signed-in user. I am using windows authentication, and I have a base controller where I am setting the logged in Windows user first name and last name in view bags. I use them all over the place, so I know the view bags are not the issue. But linq won't let me use a view bag in my statement. I tried to put the viewbag in a variable, and then use the variable in the statement but that failed as well.
"An expression tree may not contain a dynamic operation" is my error.
I've seen some similar questions, but none with current syntax. Is there a workaround to use a viewbag inside a linq statement? Or even just a workaround to use my current logged in Windows user in the linq statement?
//my linq statement
var Rad = _context.vwAssignedNotCompleted
.FirstOrDefault(x => x.AssignedToLName ==
#ViewBag.LastName && x.AssignedToFName ==
#AssignedToFName);
//My ViewBags
UserPrincipal user = UserPrincipal.Current;
var groups = user.GetAuthorizationGroups();
ViewBag.LastLogon = user.LastLogon;
ViewBag.DisplayName = user.DisplayName;
ViewBag.FirstName = user.GivenName;
ViewBag.LastName = user.Surname;
ViewBag.WSIP = user.Description;
The expected result is that I get back the database data for the current logged in Windows user
I wrote a simple test, and my code works fine in a razor:
var name = (string)ViewBag.Name ?? "";
var profile = context.Profiles.FirstOrDefault(x => x.Name == name);
I'm optimizing some code we use to query Active Directory. One of the methods fetches all of the AD users who have changed since a particular update, determined by the uSNCreated property of the Directory Entry. Essentially it's doing the C# equivalent of:
select * from PrincipalSearcher where uSNCreated > somevalue
The code is (more or less):
public IEnumerable<UserPrincipal> GetUpdatedUsers(string samAccountName, long lastUsnChanged)
{
using (var context = new PrincipalContext(ContextType.Domain))
using (var userSearcher = new PrincipalSearcher(new UserPrincipal(context)))
{
var items = userSearcher.FindAll().Cast<UserPrincipal>();
return items.Where(x => GetUsnChanged(x) > lastUsnChanged).ToArray();
}
}
private static long GetUsnChanged(Principal item)
{
var de = item.GetUnderlyingObject() as DirectoryEntry;
if (de == null)
return 0;
if (!de.Properties.Contains("uSNCreated"))
return 0;
var usn = de.Properties["uSNCreated"].Value;
var t = usn.GetType();
var highPart = (int)t.InvokeMember("HighPart", BindingFlags.GetProperty, null, usn, null);
var lowPart = (int)t.InvokeMember("LowPart", BindingFlags.GetProperty, null, usn, null);
return highPart * ((long)uint.MaxValue + 1) + lowPart;
}
Now this code DOES work, but the repeated calls to InvokeMember() are SLOW. What I'd like to do is get a reference to the HighPart and LowPart properties so that I can call them over and over without the overhead of needing to "rediscover" them every time when calling InvokeMember().
I'd though that I could do something along the lines of
static PropertyInfo highProp = highProp
?? t.GetProperty("HighPart", BindingFlags.GetProperty);
highPart = (int)highProp.GetValue(usn);
Unfortnately t.GetProperty() always returns null. Looking at the results returned by GetProperties(), GetMethods() and GetMembers(), there doesn't seem to be a visible "HighPart" or "LowPart" that I can get to, even when using BindingFlags.NonPublic - the __ComObject simply doesn't seem to expose them (even though I can call the using InvokeMember())
Is there a way to solve this, or is it time to admit defeat?
Classes from the System.DirectoryServices.AccountManagement namespace are designed for use in simple cases, e. g. you need to find a user or group. These classes have known perfomance issues. I'd recommend using DirectorySearcher or LdapConnection/SearchRequest. In this case you can filter objects on the server, not on the client which will significantly increase performance and reduce data sent over network. Here is an example of using DirectorySearcher to find all users: Get all users from AD domain
In your case the filter will look like (&(objectClass=user)(uSNCreated>=x+1)) where x is your last usn.
Be aware that you if you track objects with usnCreated attribute you will be getting only users that were created since last usn. To track changes use usnChanged attribute
We are using B2C and storing customer numbers as a Extension field on users. A single user can have one or more customers and they are stored in a comma separated string.
What I am doing now is highly inefficient:
1. Get all Users
2. Get extension properties on each user
3. Check if they have the desired extension property and if it contains the customer I want.
4. Build a list of the users I want.
Adclient is IActiveDirectoryClient
var users = (await GetAllElementsInPagedCollection(await AdClient.Users.ExecuteAsync())).ToList();
var customersUsers = users.Where(user => user.AccountEnabled.HasValue && user.AccountEnabled.Value).Where(user =>
{
var extendedProperty = ((User) user).GetExtendedProperties().FirstOrDefault(extProp => extProp.Key == customersExtendedProperty.Name).Value?.ToString();
return extendedProperty != null && extendedProperty.Contains(customerId);
}).ToList();
I want to be able to do this in one query to ActiveDirectory using the AdClient. If I try this I get errors that the methods are not supported, which makes sense as I am assuming a query is being built behind the scenes to query Active Directory.
Edit - additional info:
I was able to query Graph API like this:
var authContext = await ActiveDirectoryClientFactory.GetAuthenticationContext(AuthConfiguration.Tenant,
AuthConfiguration.GraphUrl, AuthConfiguration.ClientId, AuthConfiguration.ClientSecret);
var url = $"https://graph.windows.net:443/hansaborgb2c.onmicrosoft.com/users?api-version=1.6&$filter={customersExtendedProperty.Name} eq '{customerId}'";
var users = await _graphApiHttpService.GetAll<User>(url, authContext.AccessToken);
However, in my example I need to use substringof to filter, but this is not supported by Azure Graph API.
I am not using that library, but we are doing a very similar search using the Graph API. I have constructed a filter that will look for users that match two extension attribute values I am looking for. The filter looks like this:
var filter = $"$filter={idpExtensionAttribute} eq '{userType.ToString()}' and {emailExtensionAttribute} eq '{emailAddress}'";
We have also used REST calls via PowerShell to the Graph API that will return the desired users. The URI with the associated filter looks like this:
https://graph.windows.net/$AzureADDomain/users?`$filter=extension_d2fbadd878984184ad5eab619d33d016_idp eq '$idp' and extension_d2fbadd878984184ad5eab619d33d016_email eq '$email'&api-version=1.6
Both of these options will return any users that match the filter criteria.
I would use normal DirectorySearcher Class from System.DirectoryServices
private void Search()
{
// GetDefaultDomain as start point is optional, you can also pass a specific
// root object like new DirectoryEntry ("LDAP://OU=myOrganisation,DC=myCompany,DC=com");
// not sure if GetDefaultDomain() works in B2C though :(
var results = FindUser("extPropName", "ValueYouAreLookingFor", GetDefaultDomain());
foreach (SearchResult sr in results)
{
// query the other properties you want for example Accountname
Console.WriteLine(sr.Properties["sAMAccountName"][0].ToString());
}
Console.ReadKey();
}
private DirectoryEntry GetDefaultDomain()
{ // Find the default domain
using (var dom = new DirectoryEntry("LDAP://rootDSE"))
{
return new DirectoryEntry("LDAP://" + dom.Properties["defaultNamingContext"][0].ToString());
}
}
private SearchResultCollection FindUser(string extPropName, string searchValue, DirectoryEntry startNode)
{
using (DirectorySearcher dsSearcher = new DirectorySearcher(startNode))
{
dsSearcher.Filter = $"(&(objectClass=user)({extPropName}={searchValue}))";
return dsSearcher.FindAll();
}
}
I came across this post looking to retrieve all users with a specific custom attribute value. Here's the implementation I ended up with:
var filter = $"{userOrganizationName} eq '{organizationName}'";
// Get all users (one page)
var result = await graphClient.Users
.Request()
.Filter(filter)
.Select($"id,surname,givenName,mail,{userOrganizationName}")
.GetAsync();
Where userOrganizationName is the b2c_extension full attribute name.
I want to list all the groups in a domain. If I use DirectorySearcher or LdapConnection and SearchRequest objects, some of the groups are missing in the returned list. But I can get the groups if I traverse all the tree with DirectoryEntry class starting from the root of the directory.
I checked the attributes of the returned and missing groups with AD Explorer tool but I could not see any difference between them. I need to use LdapConnection + SearchRequest since DirectoryEntry does not allow me to manage certificate issues if I need to use LDAP+SSL.
Did anyone enconter the same proble? What may be wrong?
Sample code for search operation;
LdapConnection _connection = new LdapConnection(new LdapDirectoryIdentifier("MTS", 389));
_connection.AuthType = AuthType.Basic;
_connection.Credential = new NetworkCredential("MTS\user1", "test123");
string _target = "dc=MTS,dc=com";
SearchRequest _request = new SearchRequest(_target, "(&(objectCategory=Group)(objectClass=group))", System.DirectoryServices.Protocols.SearchScope.Subtree, new string[] { "sAMAccountName" });
var _response = (SearchResponse)_connection.SendRequest(_request);
List<string> _namelist = new List<string>(16);
foreach (SearchResultEntry entry in _response.Entries)
{
if (entry.Attributes["sAMAccountName"].Count > 0)
_namelist.Add(entry.Attributes["sAMAccountName"][0].ToString());
}
Edit 1
If I change the search filter and search only for the missing group, it finds that group. Following search filter works and gets the group,
"(&(sAMAccountName=testGroup)(objectCategory=Group)(objectClass=group))"
There may be a limitation but I set Sizelimit and TimeLimit so high and search never returns an error about any limitation.
I had the same issue, no timeout or error but incomplete results.
I will try the PageResultRequestControl, thanks.
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