Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Complete C# Example for Obtaining ALL a User's Groups from Active Directory

2,026 views
Skip to first unread message

maggieb

unread,
Feb 11, 2005, 1:50:16 PM2/11/05
to
I had to piece this together from various articles I found about active
directory and the problem with obtaining the primary group for a user
(using memberOf doesn't do the trick). Thanks to Joe Kaplan and the
example he posted at
http://groups-beta.google.com/group/microsoft.public.active.directory.interfaces/msg/4c014b52afccd2d0?output=gplain

I was able to create this C# code which creates a GenericPrincipal
object initialized with a user's group information. Enjoy.

using System;
using System.Collections;
using System.DirectoryServices;
using System.Security.Principal;
using System.Text;

namespace DataAccess
{
/// <summary>
/// Retrieves a user and group information from Active Directory.
/// Adapted from:
http://groups-beta.google.com/group/microsoft.public.active.directory.interfaces/msg/4c014b52afccd2d0?output=gplain
/// Thanks to Joe Kaplan
/// </summary>
public class ActiveDirectoryDao
{
public ActiveDirectoryDao()
{
}

private GenericPrincipal GetWindowsIdentity(string ldapPath, string
domain, string username, string password)
{
GenericPrincipal principal = null;
string domainAndUsername = domain + @"\" + username;
DirectoryEntry entry = new DirectoryEntry(ldapPath,
domainAndUsername, password);
try
{
// Bind to the native AdsObject to force authentication.
Object obj = entry.NativeObject;

DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(SAMAccountName=" + username + ")";
search.PropertiesToLoad.Add("cn");
SearchResult result = search.FindOne();

DirectoryEntry userEntry = new DirectoryEntry(result.Path,
domainAndUsername, password);

string[] groups = GetGroups(userEntry, ldapPath);

GenericIdentity id = new GenericIdentity(username,
"LdapAuthentication");
principal = new GenericPrincipal(id, groups);
}
catch (System.Exception ex)
{
throw new System.Exception("Error authenticating user in active
directory.", ex);
}
return principal;
}

public string[] GetGroups(DirectoryEntry userEntry, string ldapPath)
{
ArrayList allGroupNames = new ArrayList();
userEntry.RefreshCache(new string[] {"tokenGroups"});
PropertyValueCollection groupSids =
userEntry.Properties["tokenGroups"];
ArrayList binarySids = new ArrayList(groupSids.Count);
binarySids.AddRange(groupSids);
for (int i = 0; i <= binarySids.Count - 1; i++)
{
byte[] binSid = ((byte[]) (binarySids[i]));
string octetSid = ConvertToOctetString(binSid);
string groupPath = string.Format(ldapPath + "/<SID={0}>",
octetSid);
DirectoryEntry groupEntry = new DirectoryEntry(groupPath,
userEntry.Username, userEntry.Password);
string groupName =
groupEntry.Properties["samAccountName"].Value.ToString();
allGroupNames[i] = groupName;
}
return (string[])allGroupNames.ToArray(typeof(string));
}

public string ConvertToOctetString(byte[] values)
{
return ConvertToOctetString(values, false, false);
}

public string ConvertToOctetString(byte[] values, bool
isAddBackslash)
{
return ConvertToOctetString(values, isAddBackslash, false);
}

public static string ConvertToOctetString(byte[] values, bool
isAddBackslash, bool isUpperCase)
{
string slash;
if (isAddBackslash)
{
slash = "\\";
}
else
{
slash = string.Empty;
}
string formatCode;
if (isUpperCase)
{
formatCode = "X2";
}
else
{
formatCode = "x2";
}
StringBuilder builder = new StringBuilder(values.Length*2);
for (int iterator = 0; iterator <= values.Length - 1; iterator++)
{
builder.Append(slash);
builder.Append(values[iterator].ToString(formatCode));
}
return builder.ToString();
}
}
}

maggieb

unread,
Feb 11, 2005, 1:54:45 PM2/11/05
to
One tiny change - the GetWindowsIdentity method should be public :)

maggieb

unread,
Feb 11, 2005, 2:05:00 PM2/11/05
to
PS - the ldapPath argument should look something like this:
LDAP://machineName.domainName.com

Joe Kaplan (MVP - ADSI)

unread,
Feb 11, 2005, 4:02:33 PM2/11/05
to
Thanks for posting Maggie!

There are a couple of things to point out here:

- This only works for security groups, not for distribution only groups
(since they aren't in the user's token and won't be in tokenGroups)
- It won't necessarily pick up groups in a different domain
- It can be made much faster doing a search for all of the groups at once
by creating a big LDAP filter with all of the group SIDs than by binding to
each one individually

Someday I'll try to post a sample showing the latter part unless you happen
to decide to try it yourself.

Joe K.

"maggieb" <mag...@obscure.org> wrote in message
news:1108147816.9...@l41g2000cwc.googlegroups.com...

maggieb

unread,
Feb 17, 2005, 12:38:04 PM2/17/05
to
Yes, I had read about the method you mention but had great difficulty
finding a good enough example of how to do it. The examples I found
most readily were in VB (not even VB.Net, and pretty cryptic). A better
example would be wonderful.

My intent with this class was to get security groups, since I use this
class specifically for security purposes.

This implementation picks up groups in the domain specified in the
ldapPath and the domain argument. I needed it for an asp.net
application that was residing in a different domain than the active
directory where the users were. By providing the full path to the
active directory domain controller in the ldapPath I was able to
retrieve info for users the different domain.

Joe Kaplan (MVP - ADSI)

unread,
Feb 17, 2005, 1:34:12 PM2/17/05
to
The main difference between your approach and the one I suggested with
looking up all the groups at once is that instead of binding to each group
via its SID in a loop, you would build a big search filter in a loop that
would look something like this:

(|(objectSid=\xx\xx\xx\xx....)(objectSid=\xx\xx\xx\xx..)....)

Then, you would use the DirectorySearcher to find all of those groups at
once and get the sAMAccountName from the SearchResult. The net result is
essentially the same, but the single search seems to be about 10x faster in
most of my tests (depending a lot on how many groups are in the token).

If you are interested in looking at it, you can try it pretty easily. I had
some sample VB.NET code (sorry, no C# handy) around that you should be able
to adapt:

Dim searcher As New DirectorySearcher
searcher.SearchRoot = root
searcher.PropertiesToLoad.AddRange(New String() {"isDeleted",
"samAccountName"})
searcher.Filter = GetFilter(sids)
searcher.CacheResults = False
searcher.SearchScope = SearchScope.Subtree

Dim results As SearchResultCollection
Dim result As SearchResult

results = searcher.FindAll()
For Each result In results
If Not result.Properties.Contains("isDeleted") Then 'this should never
happen!
groups.Add(result.Properties("sAMAccountName")(0))
End If
Next

Private Function GetFilter(ByVal sids()() As Byte) As String

Dim filter As New StringBuilder((sids.Length * 90) + 10) 'stupid guess;
not accurate

filter.Append("(|")

For i As Integer = 0 To sids.Length - 1
filter.Append("(objectSid=")
filter.Append(ConvertToOctetString(sids(i), True, True))
filter.Append(")")
Next

filter.Append(")")
Return filter.ToString()
End Function

Best of luck!

Joe K.

"maggieb" <mag...@obscure.org> wrote in message

news:1108661884.9...@l41g2000cwc.googlegroups.com...

reach...@gmail.com

unread,
Mar 15, 2005, 6:37:01 PM3/15/05
to
Maggie,
I used the "MemeberOf" property and was able to retrieve the Groups
associated with the user. I modified your GetGroups() function to do
this. The new GetGroups() function does not use the 'sids'.

public string[] GetGroups(DirectoryEntry userEntry, string ldapPath)
{
ArrayList allGroupNames = new ArrayList();

userEntry.RefreshCache(new string[] {"memberOf"});
PropertyValueCollection groups =
userEntry.Properties["memberOf"];
ArrayList groupsArray = new ArrayList(groups.Count);
groupsArray.AddRange(groups);
for (int i = 0; i <= groupsArray.Count - 1; i++)
{
string FullgroupName = (string)groupsArray[i];
string groupPath = string.Format(ldapPath + "/{0}",FullgroupName);


DirectoryEntry groupEntry = new DirectoryEntry(groupPath,
userEntry.Username, userEntry.Password);
string groupName =
groupEntry.Properties["samAccountName"].Value.ToString();

allGroupNames.Add(groupName);
}
return (string[])allGroupNames.ToArray(typeof(string));

Joe Kaplan (MVP - ADSI)

unread,
Mar 15, 2005, 10:05:41 PM3/15/05
to
Did you read my reply? MemberOf does not retrieve all groups. It ignores
nested groups and the primary group. This is why people don't use it.

Joe K.

<reach...@gmail.com> wrote in message
news:1110929821.6...@f14g2000cwb.googlegroups.com...

Nikhil

unread,
Mar 17, 2005, 8:53:53 AM3/17/05
to Joe Kaplan (MVP - ADSI)
What Joe said is true ( and has always been ;) )
You can even list the group which are nested, provided that you do it in
a recursive manner.
Joe, please correct me , if I am wrong somewhere.
here is the sample perl code ..
------------------------------
use Win32::OLE 'in';
$Win32::OLE::Warn ;
my $strUserDN = 'cn=user1,dc=example,dc=org';
my $objUser = Win32::OLE->GetObject("LDAP://$strUserDN");
my $strSpaces = "";
my $DEBUG=1;
my %dicSeenGroup=();
if(defined($objUser)) {
DisplayGroups("LDAP://$strUserDN", $strSpaces, %dicSeenGroup);
} else {
print "Check with Correct User \n" if ($DEBUG)
}

sub DisplayGroups {
print "Called Display Groups \n";
my ($strObjectADsPath, $strSpaces, %dicSeenGroup) = @_;
#print "$strObjectADsPath \n";

my $objObject = Win32::OLE->GetObject($strObjectADsPath);
my $groupName = $objObject->Name;
$groupName =~ s/^CN=//;
print "$groupName \n";;
if ($objObject->Get("memberOf")) {
$colGroups = $objObject->Get("memberOf");
}

foreach my $strGroupDN (in $colGroups) {
# print "String DN is $strGroupDN \n" if ($DEBUG);
if (not $dicSeenGroup{$strGroupDN}) {
$dicSeenGroup{$strGroupDN} = 1;
DisplayGroups("LDAP://" . $strGroupDN, $strSpaces . " ",
%dicSeenGroup);
}
}

}
-----------------------

-Nikhil

Joe Kaplan (MVP - ADSI)

unread,
Mar 17, 2005, 10:13:56 AM3/17/05
to
Recursion will expand out memberOf. However, it does not include the
primary group, so if you need that, you need to use another method.

The other potential issue with memberOf is that it includes both security
and distribution groups. Depending on what you want, this may or may not be
a good thing. If you only want security groups, then more work is needed to
filter the non-security groups out.

So, I will say that recursing over memberOf does work to expand group
membership. It is just that if you want security groups, tokenGroups seems
to me to be more more straightforward and vastly faster by the time you
verify each group's type.

I can almost read your PERL code. :)

Joe K.

"Nikhil" <mni...@gmail.com> wrote in message
news:42398BF1...@gmail.com...

phronima

unread,
Mar 31, 2005, 8:23:55 PM3/31/05
to
Hi,
My test failed in the GetGroups method, on the following line

GenericIdentity id = new GenericIdentity(username,
"LdapAuthentication");

Error: There is no such object on server

I dont know much about LDAP but the usernanme and LDAP paths are
correct as I've been stuffing around with this for 2 days now.

relevant section in my web.config just in case this could be a problem
<identity impersonate="true" />
<authentication mode="Forms">
<forms loginUrl="CCLogin.aspx" name="adAuthCookie" timeout="60">
<!-- <path="/" /> this gave me a syntax error-->
</forms>
</authentication>
<authorization>
<allow users="*" /> <!-- Allow all users -->
<!-- <allow users="[comma separated list of users]"
roles="[comma separated list of roles]"/>
<deny users="[comma separated list of users]"
roles="[comma separated list of roles]"/>
-->
</authorization>


Any ideas ?

"Joe Kaplan \(MVP - ADSI\)" <joseph....@removethis.accenture.com> wrote in message news:<e3YUxPwK...@TK2MSFTNGP15.phx.gbl>...

phronima

unread,
Mar 31, 2005, 8:21:28 PM3/31/05
to
Hi,
My test failed in the GetGroups method, on the following line
GenericIdentity id = new GenericIdentity(username,
"LdapAuthentication");

Error: There is no such object on server

I dont know much about LDAP but the usernanme and LDAP paths are
correct as I've been stuffing around with this for 2 days now.

relevant section in my web.config just in case this could be a problem
<identity impersonate="true" />
<authentication mode="Forms">
<forms loginUrl="CCLogin.aspx" name="adAuthCookie" timeout="60">
<!-- <path="/" /> this gave me a syntax error-->
</forms>
</authentication>
<authorization>
<allow users="*" /> <!-- Allow all users -->
<!-- <allow users="[comma separated list of users]"
roles="[comma separated list of roles]"/>
<deny users="[comma separated list of users]"
roles="[comma separated list of roles]"/>
-->
</authorization>


Any ideas ?

"Joe Kaplan \(MVP - ADSI\)" <joseph....@removethis.accenture.com> wrote in message news:<e3YUxPwK...@TK2MSFTNGP15.phx.gbl>...

Joe Kaplan (MVP - ADSI)

unread,
Mar 31, 2005, 8:42:27 PM3/31/05
to
A couple of questions:

Are you sure it died on that line? That doesn't make any sense at all as
you are just initializing a generic identity with two strings. Do you have
a stack trace that shows that? The error you are showing indicates a COM
exception you would get from an LDAP call if you passed in a distinguished
name for an object that does not exist. The code you show definitely isn't
responsible for that.

Why are you impersonating? Typically you don't do that with forms
authentication as anonymous auth is selected in IIS and impersonation will
result in impersonating the anonymous user. Sometimes you do want to do
that, but most of the time you don't.

Joe K.

"phronima" <phro...@gmail.com> wrote in message
news:79791d35.05033...@posting.google.com...

Joe Richards [MVP]

unread,
Apr 3, 2005, 9:26:06 AM4/3/05
to
Ah but what JoeK was referencing is that this doesn't pick up all membership in
all domains, only the membership of the user in the domain the user exists in.
For instance, if the user is in a domain in a forest with 12 domains and is in
domain local groups in every domain, you will only list the groups the user is
in in that one domain of the 12. Your subject is "Obtaining ALL a User's Groups
from Active Directory" which would imply all group memberships in all domains.
This clearly doesn't fit the bill.

This really isn't a trivial subject as JoeK is pointing out. It is trivial only
in the case of security groups for a single domain that the user is a member of.
Even then, it can be rather involved resolving the SIDs to names. As JoeK
pointed out there is a quicker way than enumerating all objects by individually
binding to them by the SID= mechanism but you also have to take care there that
your query doesn't get too large. Chances are, it generally won't be but it is
something to keep in mind that could happen.

joe

--
Joe Richards Microsoft MVP Windows Server Directory Services
www.joeware.net

Joe Kaplan (MVP - ADSI)

unread,
Apr 3, 2005, 12:43:12 PM4/3/05
to
Hey Joe,

Agreed that this problem is a hard one and often times when people ask for a
solution, they aren't necessarily clear on what it is exactly that they
want. All group memberships in the forest or just the current domain? Just
security groups or DLs too? Only DLs that are mail-enabled in Exchange?
What do we do about FSPs?

Do you ever use Attribute-Scoped Query for doing recursive group unwinding?
This feature seems taylor-made for doing this kind of stuff (search within
the member attribute for additional objects that are also groups). It is
2003-only, but it is still a cool feature. There are finally nice wrappers
for this in .NET 2.0, so the functionality will finally be available to
non-C++ languages and will get more support.

The scripters haven't been invited to the party yet, but they'll likely be
missing all the cool new stuff for a while until the whole monad thing
matures. The SDK team seems to be focusing nearly 100% on improving the
managed code interfaces these days.

Joe K.

"Joe Richards [MVP]" <humore...@hotmail.com> wrote in message
news:OwjOyCFO...@tk2msftngp13.phx.gbl...

Joe Richards [MVP]

unread,
Apr 3, 2005, 1:57:26 PM4/3/05
to
I haven't played with the ASQs yet. Once we get more penetration of K3 I will
probably start using it consistently. I hate having multiple code paths based on
versions so try to avoid it.

joe

--
Joe Richards Microsoft MVP Windows Server Directory Services
www.joeware.net

Joe Kaplan (MVP - ADSI) wrote:

phronima

unread,
Apr 3, 2005, 7:58:02 PM4/3/05
to
"Joe Kaplan \(MVP - ADSI\)" <joseph....@removethis.accenture.com> wrote in message news:<#6C0QwlN...@TK2MSFTNGP15.phx.gbl>...

> A couple of questions:
>
> Are you sure it died on that line? That doesn't make any sense at all as
> you are just initializing a generic identity with two strings. Do you have
> a stack trace that shows that? The error you are showing indicates a COM
> exception you would get from an LDAP call if you passed in a distinguished
> name for an object that does not exist. The code you show definitely isn't
> responsible for that.
>
> Why are you impersonating? Typically you don't do that with forms
> authentication as anonymous auth is selected in IIS and impersonation will
> result in impersonating the anonymous user. Sometimes you do want to do
> that, but most of the time you don't.

Hi, Thanks for your reply.

Having had a closer look I realised where the problem really is:

string groupPath = string.Format(ldapPath + "/<SID={0}>", octetSid);


DirectoryEntry groupEntry = new DirectoryEntry(groupPath,
userEntry.Username, userEntry.Password);
string groupName =groupEntry.Properties["samAccountName"].Value.ToString();

It failed on the last line because there is no samAccountName in the
groupEntry properties. In fact there are no valid properties in
groupEntry (see stack trace below)

By distinguished user name do you mean CN? I simply used my windows
logon and password. Do I need to be an any specific Administration
group on the AD?

groupPath = "LDAP://machine.domain.com/<SID=010500000000000515000000160b3c6a0840cc693e4abd5fc6040000>"

##STACK TRACE####
at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
at System.DirectoryServices.DirectoryEntry.Bind() at
System.DirectoryServices.DirectoryEntry.get_AdsObject() at
System.DirectoryServices.PropertyCollection.get_Count() at
in2lifeCC.CCLogin.GetGroups(DirectoryEntry userEntry, String ldapPath)
in c:\inetpub\wwwroot\cc\cclogin.aspx.cs:line 262 at
in2lifeCC.CCLogin.GetWindowsIdentity(String ldapPath, String domain,
String username, String password) in
c:\inetpub\wwwroot\cc\cclogin.aspx.cs:line 233

Runtime values in the groupEntry
adsObject <undefined value>
AdsObject <error: an exception of type:
{System.Runtime.InteropServices.COMException} occurred>

Any ideas? I wonder how I can explain my technical difficulty against
my estimated time, which by now is well overrun..;-)

Joe Kaplan (MVP - ADSI)

unread,
Apr 4, 2005, 2:22:54 PM4/4/05
to
By distinguished name, I mean the part in the LDAP path here:
LDAP://<server>/<distinguished name>

The server part isn't always used, but DN usually is. DN usually looks like
CN=someone,CN=users,DC=domain,DC=com or something like that and uniquely
specifies an object in the directory. <SID=xxxxxx> is an alternate form of
DN that Active Directory supports for referencing an object directly by its
SID. CN is just the common name and can be used to uniquely identify an
object in its current container, but does not necessarily uniquely identify
an object in the whole directory. In the above DN, CN=someone identifies
that object in the CN=users container, but there could be another CN=someone
in another part of the directory.

My guess is that the error you are getting is related to security somehow.
Is it possible that the ID and password you are using doesn't have access to
view that particular security group in the directory? If you built the SID
correctly, then that binding string should work.

I'd suggest checking this in a low level tool like LDP.exe. You can bind
with those credentials to AD and then paste that <SID=xxx> DN into the View
| Tree... dialogue and the group should come right up.

Otherwise, I'm not sure why it isn't working.

Joe K.


"phronima" <phro...@gmail.com> wrote in message

news:79791d35.05040...@posting.google.com...

phronima

unread,
Apr 5, 2005, 2:02:33 AM4/5/05
to
Just wanted to say that using memberOf instead of tokengroups I was
able to retrieve the correct information. I am guessing its because
there are no nested groups in this domain..

Joe Kaplan (MVP - ADSI)

unread,
Apr 5, 2005, 12:34:11 PM4/5/05
to
If you can make that assumption and can do without the primary group too,
then that can work. It just depends on your needs.

The tokenGroups thing works in general, so I think we could have solved that
for you had we tried harder.

Joe K.

"phronima" <phro...@gmail.com> wrote in message
news:79791d35.05040...@posting.google.com...

phronima

unread,
Apr 6, 2005, 12:17:25 AM4/6/05
to
"Joe Kaplan \(MVP - ADSI\)" <joseph....@removethis.accenture.com> wrote in message news:<OK2bL1fO...@TK2MSFTNGP09.phx.gbl>...

My question at this stage is: if it had been a security issue how was
I able to have it working with memberOf but failed when using
tokengroups?

Joe Kaplan (MVP - ADSI)

unread,
Apr 6, 2005, 10:13:45 AM4/6/05
to
I don't know why the SID bind did not work. My guess is that there is a
syntax problem in the <SID=> DN. If I saw your complete code sample, that
might help. There is no reason why it should not work though.

Joe K.

"phronima" <phro...@gmail.com> wrote in message
news:79791d35.05040...@posting.google.com...
>

phro...@gmail.com

unread,
Apr 8, 2005, 1:55:13 AM4/8/05
to
It's the exact same code that was provided by maggie (thanks maggie). I
used my windows logon credentials.

Joe Kaplan (MVP - ADSI) wrote:

Joe Kaplan (MVP - ADSI)

unread,
Apr 8, 2005, 10:23:33 AM4/8/05
to
Does it fail for all of the SIDs or just some of them? Can you show a
simple code sample that demonstrates the problem? It is hard to diagnose it
like this.

Also, why don't you start a new thread as this one has gotten very deep.

Joe K.

<phro...@gmail.com> wrote in message
news:1112939712.9...@f14g2000cwb.googlegroups.com...

0 new messages