Gabe's Code

Stuff I've learned along the way

Active Directory

more...

profile for Gabriel Luci at Stack Overflow, Q&A for professional and enthusiast programmers

Active Directory: Find out if one user is a member of a group

This article will discuss figuring out if a specific user is a member of a specific group. That is, if you already know who the user is and what the group is. But before learning that, it’s helpful to know just what makes a user a member of a group. If you haven’t read that article yet, do that first:

What makes a member a member?

The easy way

Whatever type of code you’re writing, if you’re using Windows Authentication, there will usually be a way to get an object for the currently-authenticated user. For example, in an ASP.NET application that is using Windows Authentication, you can use this to determine if the currently-authenticated user is a member of a group:

var isMember = HttpContext.Current.User.IsInRole("DOMAIN\\GroupName");

Or in a desktop application, you can use this:

var principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
var isMember = principal.IsInRole("DOMAIN\\GroupName");

Even if you’re not using .NET, there will usually be some equivalent.

.NET’s WindowsPrincipal.IsInRole is designed for testing authorization: whether a person should be granted permissions that are granted to the group. Thus, it does work for nested membership (if the account is a member of a group that is a member of the group in question). It is fairly quick too, since it uses the account’s authentication token to test the membership, which is already in memory. If you’re curious, under the hood it uses Windows’ built-in function called CheckTokenMembership.

The caveat is that IsInRole (or any method designed for authentication) will not work for groups that are:

The slightly harder but still easy way

While the following code is in C#, the principals used can usually be adapted to any language that can query LDAP. This method will work regardless of:

private static bool IsUserInGroup(DirectoryEntry user, DirectoryEntry group, bool recursive) {

    //fetch the attributes we're going to need
    user.RefreshCache(new [] {"distinguishedName", "objectSid"});
    group.RefreshCache(new [] {"distinguishedName", "groupType"});

    //This magic number tells AD to look for the user recursively through any nested groups
    var recursiveFilter = recursive ? ":1.2.840.113556.1.4.1941:" : "";

    var userDn = (string) user.Properties["distinguishedName"].Value;
    var groupDn = (string) group.Properties["distinguishedName"].Value;
    
    var filter = $"(member{recursiveFilter}={userDn})";

    if (((int) group.Properties["groupType"].Value & 4) == 4) {
        var groupDomainDn = groupDn.Substring(
            groupDn.IndexOf(",DC=", StringComparison.Ordinal));
        var userDomainDn = userDn.Substring(
            userDn.IndexOf(",DC=", StringComparison.Ordinal));
        if (groupDomainDn != userDomainDn) {
            //It's a Domain Local group, and the user and group are on
            //different domains, so the account might show up as a Foreign
            //Security Principal. So construct a list of SID's that could
            //appear in the group for this user
            var fspFilters = new StringBuilder();
            
            var userSid =
                new SecurityIdentifier((byte[]) user.Properties["objectSid"].Value, 0);
            fspFilters.Append(
                $"(member{recursiveFilter}=CN={userSid},CN=ForeignSecurityPrincipals{groupDomainDn})");
            
            if (recursive) {
                //Any of the groups the user is in could show up as an FSP,
                //so we need to check for them all
                user.RefreshCache(new [] {"tokenGroupsGlobalAndUniversal"});
                var tokenGroups = user.Properties["tokenGroupsGlobalAndUniversal"];
                foreach (byte[] token in tokenGroups) {
                    var groupSid = new SecurityIdentifier(token, 0);
                    fspFilters.Append(
                        $"(member{recursiveFilter}=CN={groupSid},CN=ForeignSecurityPrincipals{groupDomainDn})");
                }
            }
            filter = $"(|{filter}{fspFilters})";
        }
    }

    var searcher = new DirectorySearcher {
        Filter = filter,
        SearchRoot = group,
        PageSize = 1, //we're only looking for one object
        SearchScope = SearchScope.Base
    };

    searcher.PropertiesToLoad.Add("cn"); //just so it doesn't load every property

    return searcher.FindOne() != null;
}

This method works by searching for groups that have the user as a member. But since we set SearchRoot to the group itself, it is only possible for that one group to be returned. So the search has two possible outcomes:

  1. If the user is a member, the group is returned
  2. Otherwise, nothing is returned

Hence, we only need to test if something was returned.

The recursive option works by using one of a few magic numbers called Matching Rule OIDs. This specific one (1.2.840.113556.1.4.1941) is called LDAP_MATCHING_RULE_IN_CHAIN. It can only be used on attributes that accept distinguished names (like member). It tells Active Directory to follow the chain of groups to find the user; for example, if the user is a member of a group that is a member of the group in question.

More information can be found on Microsoft’s article on their LDAP Search Filter Syntax.

There is a bit of magic in here for Foreign Security Principals; that is, if the user could be on an external trusted domain from the group. So it constructs the distinguishedName of the FSP that would be there if that were the case. For a recursive search, we use tokenGroupsGlobalAndUniversal to get a recursive list of all the user’s groups from the user’s domain and see if any of those are in the group in question.

The tokenGroupsGlobalAndUniversal attribute contains only Global and Universal groups. Domain Local groups cannot be used outside of the domain anyway, so we don’t need to be concerned with those. The tokenGroups attribute is similar. It does include Domain Local groups, however it excludes Distribution groups, which is why I didn’t use it here.

Primary Group

If you read my What makes a member a member? article, then you’ll know that a user’s primary group is not governed by the member attribute of the group, so the above method won’t work if you need to test if a group is the user’s primary group. Granted, that’s a pretty rare need, so you may be able to ignore this altogether.

But if you do need to find out if a group is a user’s primary group, this is how you can do it:

private static bool IsUserPrimaryGroup(DirectoryEntry user, DirectoryEntry group) {
    user.RefreshCache(new[] {"primaryGroupID", "objectSid"});
    group.RefreshCache(new[] {"objectSid"});

    //Get the SID's as a string
    var userSid =
        new SecurityIdentifier((byte[])user.Properties["objectSid"].Value, 0).ToString();
    var groupSid =
        new SecurityIdentifier((byte[])group.Properties["objectSid"].Value, 0).ToString();

    //Replace the RID portion of the user's SID with the primaryGroupId
    //so we're left with the primary group's SID
    var primaryGroupSid =
        userSid.Remove(userSid.LastIndexOf("-", StringComparison.Ordinal) + 1)
        + user.Properties["primaryGroupId"].Value;

    return groupSid == primaryGroupSid;
}

22 comments

Hi,

Can you plz tell me how can we find a user in a container group (organizational unit) or not?

Bonuj

There is a simple example here. Basically, you set the OU as the SearchRoot and it will search only that OU and below. If you need more specific help, ask a question on StackOverflow. If you tag your question with active-directory, I will probably see it.

Gabe,
I want to use your pretty code in my asp.net Web-Page but it doesn’t work correctly.
I got an “CS1056 Unexpected character ‘’ on this code …
What am I doing wrong ? Thanks in advance for your reply.
BR Robert

Maybe it picked up a weird character while copy/pasting. It will tell you the line that it’s complaining about, so look at that line carefully.

Gabe, Sorry my first post was not complete:

function = IsUserInGroup()
first syntax, compiler problem: var filter = $”(member{recursiveFilter}={userDn})”;

I have also looked around and other users in other, but similar cases wrote that the problem could be fixed by installing a Nuget package.
Anyway, my own abilities exceed this aim :-(

BR

Krissy Kernan on

Hi Gabe,

I’m trying to use your IsUserInGroup() code. I converted it to VB.net, but I’m not sure how to pass the DirectoryEntry user and group to the function. Can you please share that line of code?

My question is in regards to the user and group variables of type DirectoryEntry that are used in the function call.

I am currently using DirectoryEntry and DirectorySearcher to authenticate an AD user. To locate the user’s group, I was using windowsPrincipal.IsInRole. That works fine if the application is pointed to the AD domain that I am logged into, but it doesn’t work when the application is pointed to a different trusted domain. I’m trying to find another way to verify a user’s groups.

When your function refers to “DirectoryEntry user”, is that referring to the username property of DirectoryEntry? In regards to “Directory Entry group”, is that a second DirectoryEntry object? The only other way I know to get AD groups is to do a DirectorySearcher with the property of “memberOf”. The issue with that approach is that I need to search for parent and child groups which can be performance intensive. I’m trying to test this code to see if it’s a better option for me. Thanks for the assistance.

When your function refers to “DirectoryEntry user”, is that referring to the username property of DirectoryEntry? - No, it’s referring to the DirectoryEntry object itself, not a property of it.

The group parameter is a second object. This function checks if a user is a member of a specific group, so you need to tell it which group you are looking for. You create a DirectoryEntry for a group the same way you do for a user. For example, New DirectoryEntry("LDAP://CN=MyGroup,OU=Groups,DN=example,DC=com").

If you’re looking at an external, trusted domain, then you can include the domain DNS name (or even a specific domain controller) in the LDAP path, like this: New DirectoryEntry("LDAP://example.com/CN=MyGroup,OU=Groups,DN=example,DC=com")

If you don’t know the group, then this isn’t the method you should be using. Maybe you want to find all of the user’s groups.

Doug Belkofer on

In the code that recursively searches if the user is a member in the group, this section appears:

if (((int) group.Properties["groupType"].Value & 8) == 0) {
    var groupDomainDn = groupDn.Substring(
        groupDn.IndexOf(",DC=", StringComparison.Ordinal));
    var userDomainDn = userDn.Substring(
        userDn.IndexOf(",DC=", StringComparison.Ordinal));
    if (groupDomainDn != userDomainDn) {
        //It's a Domain Local group, and the user and group are on

You’re checking if the group type bitwise anded with 8, which is the bitmask for universal groups, based on my understanding, but your comment at the end says “It’s a Domain Local group”. My understanding is that the bitmask for Domain Local groups is 4, not 8.

Can you clarify which part is correct, or if my understanding is wrong?

My understanding is based on this link:
https://ldapwiki.com/wiki/GroupType

That if statement is saying “if it’s not a universal group”. Then it checks if the user and group are on different domains. A global group could pass the “not universal” test, but global groups can’t have members from other domains. So if we get into the second if, then we know it’s domain local. But you are right that the if could be simplified to just check if it’s a domain local group right away rather than “not a universal group”. I’ll update that to be:

if (((int) group.Properties["groupType"].Value & 4) == 4)
Doug Belkofer on

I thought of a scenario that I think your code doesn’t handle. What if I’m checking if a user in domain A is in a group in domain B, and the domain B group is the primary group of the FSP of the user in domain B?

In other words, A\johndoe has an FSP in domain B, B\S-1-2-blahblah, and the group you’re checking is B\Domain Users, and B\Domain Users is the primary group of B\S-1-2-blahblah.

I’ve modified my code to add an additional step to check if the FSP’s primary group matches the group being checked to cover this, but you have to retrieve the primary group ID of the user’s FSP to do it.

Doug Belkofer on

While testing my new code, I discovered something I wasn’t expecting - Domain Users is not a domain local group. It has the Global Group bit set, and the Security group bit set (0x80000002). So it looks like that group type should also be considered as a possible group that can have FSP’s as members. I think maybe if it has 0x2 set or 0x4 set - does that sound right to you?

Doug Belkofer on

Ugh… just realized that foreign security principals do not have a primary group. Well, at least the question about global groups possibly having FSP’s as members is still valid - can a foreign security prinicipal be added to Domain Users?

That isn’t something that can happen, so you don’t have to worry about it. 🙂 Global groups cannot contain users from other domains, which also means they cannot contain foreign security principals (even though the FSP object is on the same domain). And foreign security principals don’t have a primaryGroupID attribute.

Doug Belkofer on

OK, I think I thought of a real-world scenario that your example IsUserInGroup() method won’t handle. User U1 is a member of domain A, and is in A’s Domain Users group, which is the user U1’s primary group. A\Domain Users is a member of group G2 in domain B, via a foreign security principal that is associated to A\Domain Users.

I’ve actually seen this at my job, so I know it can happen. Also, my experience has been that by default, user membership in the Domain Users group is almost always via it being their primary group. Yes, a user can have a different group as their primary group, but it starts out as Domain Users when the user is created, at least with Active Directory.

It seems to be able to be addressed fairly simply - you just need to update the code that adds all of the groups the user is a member of to the filter to also add the user’s primary group, to cover the case where the user’s primary group is a member of the other domain group.

Finally, I want to thank you again for sharing all of this code and very understandable descriptions and explanations. All of it really helped me better understand the nuances of user & group membership.

Ian Dubé on

Hi Gabriel,
I’m trying to use your function (the one with “tokenGroupsGlobalAndUniversal”) to find if a user in Domain B, is a member of the group in domain A (and both domain are in the same Forest).

If I check manually the member of the group, I can find my user (he is 2 level lower, in a group who is in another who is in my wanted group) but your function return a result as if the user isn’t a member.

I know that if I call “GetAuthorizationGroups” using my user principal, my group will be returned correctly (but the performance are horrible, it’s why i’m trying to use a DirectorySearcher instead).

Everything i’ve read seems to say that it should work but it’s not and I have no idea why. XD

Do you have an idea of what might happen here ?
Thanks

Interesting. I can try to replicate that. So DomainB\User is a member of Group1, and Group1 is a member of DomainA\Group2, and you are checking if DomainB\User is a member of DomainA\Group2, correct? I have a few questions:

  1. What is the scope of Group1 and Group2? (domain local, global, universal)
  2. Is the “Group Type” of both Group1 and Group2 set to “Security”?
  3. Which domain is Group1 on?
  4. You are passing true for the recursive parameter?
Ian Dubé on

Almost that, in my case it’s more {DomainB\User} is a member of {DomainA\Group3}, and {DomainA\Group3} is a member of {DomainA\Group2}, which is a member of {DomainA\Group1}. And I’m trying to check if {DomainB\User} is a member of {DomainA\Group1} (but with recursive, one more level should not make a difference).

Here the info I have:

  1. {DomainA\Group1} groupScope is Local. It’s member {DomainA\Group2} groupScope is Universal. And {DomainA\Group3} (which is a member of Group2) groupScope is Global (and my DomainB\User is a member of Group3).
  2. I didn’t see the “Group Type” exactly but Group1, Group2 and Group3 have a flag {IsSecurityGroup} and it’s true for the 3 groups so I would say yes.
  3. Group1 is in DomainA (same as all the other group, only my user is in another Domain. I tested with a user in the same Domain as all the group and everything works perfectly in that case).
  4. Yes I have the recursive parameter set to true. (no choice, the security group only contains group, the user are always at least 1 or 2 layers under the main security group I’m looking for permission)

I also know that when testing, I’ve seen the groupSid of Group2 and Group3 in the {tokenGroupsGlobalAndUniversal} of DomainB\User and they are correctly listed in {fspFilters} and added to the filter for {DirectorySearcher}.

It’s strange that the searcher can’t find something when the search root is {DomainA\Group1} and that {DomainA\Group2} his a member (and his groupSid is listed in the filter) but it still find nothing.

Thanks for the help.

Ian Dubé on

Hey I don’t know if it change something but I’ve found something I didn’t see before in structure.

I was a bit wrong, { Group3 } is in { DomainB }, it’s just that I have another group with the exact same name in DomainA (who contains other user, but not the one I look for, mine is in DomainB/Group3). Both group with the same name are member of DomainA\Group2.

The sId are different and it’s the correct one that is listed in { tokenGroupsGlobalAndUniversal } and then added in the filter.

I’m not sure if that change anything since the code use the sId in the filter instead of the name so as far as I know, it should work even if 2 group in 2 domain have the same name.

So in the end, my structure look like this

DomainA\Group1 -> DomainA\Group2 -> DomainB\Group3 -> DomainB\User
(The scope are the same as previously stated)

And i’m trying to find if my user is member of Group1 using “ IsUserInGroup “ (which return false right now).

Leave a comment

Your email address is used to display your Gravatar, if applicable, and subscribe you to replies using the Mailgun web service, which you are free to unsubscribe from when you get any emails. Your email address will not be displayed publicly or shared with anyone else.
Comments are moderated. Your comment will be reviewed by a human before being posted to this page. Any comment made for the purpose of advertising will not be approved.