Skip to main content

This "tokenGroups" attribute holds a security identifier (SID) for each security group (including the primary group) for which the user is a member, including nested group membership. Recursive solutions can often be a little messy. As such, the only advantage that the recursive technique holds is that it will expand group membership in Distribution Lists, while the tokenGroups attribute contains only security group membership.

// ----------------------------------------------------------------------------
// Using LDAP Search to Retrieve a User's Token Groups
//
// This "tokenGroups" attribute holds a security identifier (SID) for each
// security group (including the primary group) for which the user is a member,
// including nested group membership.
//
// Recursive solutions can often be a little messy. As such, the only advantage
// that the recursive technique holds is that it will expand group membership in
// Distribution Lists, while the tokenGroups attribute contains only security
// group membership.
//
// The following technique uses an LDAP search to find each SID in the
// tokenGroups attribute.
//
// ----------------------------------------------------------------------------
// Addison Wesley - The .NET Developer's Guide to Directory Services Programming
// Chapter 10: Determining User Group Membership in Active Directory and ADAM
// Released May 2006
// Publisher(s): Addison-Wesley Professional
// ISBN: 0321350170
// ----------------------------------------------------------------------------

//
// 1. Load the tokenGroups attribute into the property cache
// -----------------------------------------------------------------------------
// Regardless of the technique chosen to decode the tokenGroups attribute, we
// must First retrieve it. Since this is a constructed attribute, we must use
// the RefreshCache technique to first load the attribute into the property
// cache in a DirectoryEntry object. This is one of the few attributes that
// requires a Base search scope with DirectorySearcher, so we will generally
// choose to use a DirectoryEntry instance for this work instead:

// user is a DirectoryEntry
user.RefreshCache(new string[] { "tokenGroups" });

// now the attribute will be available
int count = user.Properties["tokenGroups"].Count;
Console.WriteLine("Found {0} Token Groups", count);

//
// 2. Retrieving Token Groups with an LDAP Search
// -----------------------------------------------------------------------------
// The big upshot to this approach is that this technique is pretty fast and we
// don't have to worry aboutusing any P/Invoke code that can be intimidating to
// less-experienced developers. We simply iterate through the returned attribute
// and build a large LDAP filter that represents each security group. Once
// webuild the filter, we can easily search the domain for the groups and return
// each one. Listing 10.19 shows how we can accomplish this.

var sb = new StringBuilder();
sb.Append("(|"); // we are building an '|' clause
foreach (byte[] sid in user.Properties["tokenGroups"])
{
    // append each member into the filter
    sb.AppendFormat("(objectSid={0})", BuildFilterOctetString(sid));
}
sb.Append(")"); // end our initial filter

DirectoryEntry searchRoot = new DirectoryEntry("LDAP://DC=domain,DC=com", null, null, AuthenticationTypes.Secure);
using(searchRoot)
{
    // we now have our filter, we can just search for the groups
    var ds = new DirectorySearcher(searchRoot, sb.ToString());
    using(SearchResultCollection src = ds.FindAll())
    {
        foreach (SearchResult sr in src)
        {
            // here is each group
            Console.WriteLine(sr.Properties["samAccountName"][0]);
        }
    }
}

private static string BuildFilterOctetString(byte[] bytes)
{
    var sb = new StringBuilder();
    for (int i = 0; i < bytes.Length; i++)
    {
        sb.AppendFormat("\\{0}", bytes[i].ToString("X2"));
    }
    return sb.ToString();
}

// -----------------------------------------------------------------------------
// Complete solution from book source code downloads
// Path: DotNetDevGuide.DirectoryServices\Chapter10\Program.cs
// -----------------------------------------------------------------------------

/// <summary>
/// Listing 10.19 Modified.  Uses LDAP search to get tokenGroups
/// </summary>
[Test]
public void ExpandTokenGroups()
{
    //point this to a user in the directory (TestUtils.Settings.DefaultPartition == "dc=domain,dc=com")
    DirectoryEntry user = TestUtils.CreateDirectoryEntry(
                              "CN=User1,OU=Users," + TestUtils.Settings.DefaultPartition);

    using(user)
    {
        StringBuilder sb = new StringBuilder();

        //we are building an '|' clause
        sb.Append("(|");

        foreach (byte[] sid in user.Properties["tokenGroups"])
        {
            //append each member into the filter
            sb.AppendFormat(
                "(objectSid={0})", BuildFilterOctetString(sid));
        }

        //end our initial filter
        sb.Append(")");

        // (TestUtils.Settings.DefaultPartition == "dc=domain,dc=com")
        DirectoryEntry searchRoot = TestUtils.GetDefaultPartition();

        using(searchRoot)
        {
            //we now have our filter, we can just search for the groups
            DirectorySearcher ds = new DirectorySearcher(
                searchRoot,
                sb.ToString(), //our filter
                null,
                SearchScope.Subtree
            );

            ds.PageSize = 500;

            using(SearchResultCollection src = ds.FindAll())
            {
                foreach (SearchResult sr in src)
                {
                    //Here is each group now...
                    Console.WriteLine(sr.Path);
                }
            }
        }
    }
}

/// <summary>
/// Listing 4.2 repeated
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
private string BuildFilterOctetString(byte[] bytes)
{
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < bytes.Length; i++)
    {
        sb.AppendFormat(
            "\\{0}",
            bytes[i].ToString("X2")
        );
    }
    return sb.ToString();
}

/// <summary>
/// Listing 10.21 - uses new 2.0 features to accomplish same thing.
/// Only works with AD.
/// </summary>
[Test]
public void ExpandTokenGroupsFx2()
{
    // point this to a user in the directory (TestUtils.Settings.DefaultPartition == "dc=domain,dc=com")
    DirectoryEntry user = TestUtils.CreateDirectoryEntry("CN=User1,OU=Users," + TestUtils.Settings.DefaultPartition);

    using(user)
    {
        // we use the collection in order to
        // batch the request for translation
        IdentityReferenceCollection irc = ExpandTokenGroups(user).Translate(typeof(NTAccount));
        foreach (NTAccount account in irc)
        {
            Console.WriteLine(account);
        }
    }
}

// Sample Helper Function used by Listing 10.21
private IdentityReferenceCollection ExpandTokenGroups(DirectoryEntry user)
{
    user.RefreshCache(new string[] { "tokenGroups" });

    IdentityReferenceCollection irc = new IdentityReferenceCollection();

    foreach (byte[] sidBytes in user.Properties["tokenGroups"])
    {
        irc.Add(new SecurityIdentifier(sidBytes, 0));
    }

    return irc;
}