Use PHP and LDAP to list members of an Active Directory group – version 2

Please see the UPDATED version of this script here

PHP function that gets the members of an Active Directory group, and returns the Users’ attributes as an array.

The Function

Example Output

89 Comments

  1. Lachlan

    this works great, and has saved me a lot of time, thanks!
    I don’t suppose there is a way to search for multiple groups at a time?

    • sam

      Lachlan,

      I have updated the original post with a newer version of the function that allows you to specify multiple groups

      That should work for you

      • Lachlan

        Thanks for that Sam… unfortunately, it does not work.
        when using it without specifying any groups, it works fine, but if I specify a group (exactly as provided in the example), I get the following errors:
        array_shift() expects parameter 1 to be array, null given in C:\inetpub\wwwroot\test.php on line 68
        Invalid argument supplied for foreach() in C:\inetpub\wwwroot\test.php on line 74
        the only change I made to your code when testing was I changed username/password/domain details (obviously) and changed the attributes it searches for. I even tried setting them back to as above, but to no avail.

        • sam

          The error was occurring when a ‘keep’ attribute was not found for a user (for example: a user had no telephone number value)

          I have fixed the original post, the tweaked code is:

      • Yogesh

        Hi Sam,

        Thanks for this great post!

        I’m actually very new to LDAP and I would require your help in solving a small problem.

        I’m using OpenLDAP on CentOS 6 and I’ve created a structure like this:

        -dc=company,dc=com
        –ou=Unit1
        —cn=Admin
        —-uid=username1
        —-uid=username2
        —-uid=username3

        —cn=PowerUser
        —-uid=username4
        —-uid=username5
        —-uid=username6

        —cn=User
        —-uid=username7
        —-uid=username8
        —-uid=username9

        —cn=Guest
        —-uid=username10
        —-uid=username11
        —-uid=username12

        –ou=Unit2
        —cn=Admin
        —-uid=username13
        —-uid=username14
        —-uid=username15

        —cn=PowerUser
        —-uid=username16
        —-uid=username17
        —-uid=username18

        —cn=User
        —-uid=username19
        —-uid=username20
        —-uid=username21

        —cn=Guest
        —-uid=username22
        —-uid=username23
        —-uid=username24

        i.e. I have different organisational units (e.g. corresponding to different departments).
        Within each OU, I have 3 CN (corresponding to different user groups within each department) and within each user group, I have my users. I have assigned a password to each UID.

        1. Is this type of structure correct or is there a much simpler way to have different user groups in each department of the company ?

        2. Now, let’s say I have a login form. What would be the algorithm to validate the username and password based on the above structure ? i.e. when a user inputs his UID and password on the login form, how do I know
        a) if the user exists?
        b) in which department and user group the user is found?

        Based on the above, I’ll be able to enable/disable different modules on the company intranet so that the authenticated user can only access his/her assigned modules.

        Thanks in advance for your help.

        Best regards,
        Yogesh.

        • sam

          1. It depends on if Admin/Power User/User/Guest are specific to each Unit. If they apply to the company as a whole, it might make more sense to have one set of those groups, and then a member could belong to both the general ‘Power User’ account and a department like ‘Unit1’ for example

          2. The user account is checked for existence in the bind action– then you can run a query to check memberOf to track down which department and group they belong to

          • Yogesh

            Hi Sam,

            Many thanks for your reply.

            Can you please give an example of the point you mentioned in (1)?

            How would you structure the LDAP directory?

            Can you please also provide an example of your solution of point no. (2)?

            Thanks in advance.

          • Yogesh

            Sorry, I forgot to precise. Yes, in each department (i.e. Organisation Unit – HR, IT, Finance, Sales, etc) there will be different categories of users, namely Admins, Power Users, Simple Users (i.e. those who do the daily “clerk” stuff) and Guest (i.e. those who have almost the same rights as the Simple Users, but can only read. There are some tasks and/or modules which Simple Users can access and execute.

            Thanks for your kind cooperation.

            Best regards,
            Yogesh.

          • sam

            It sounds like you have the right structure then. I am not an expert on the best AD structures for an organization. At my office we do a similar structure as yours– with ‘HR Admins’ nested inside the ‘HR’ group for example

            The code in this article may better answer your #2:
            http://samjlevy.com/?p=613

            You can rely on the ldap_bind to verify the account and password– then a query (ldap_search) is necessary to get membership information

  2. Rasmus Lund

    Hey Sam

    Excellent code work. I build an application for a business in Denmark, where they must have 2 different logins. Admin and User login. I do not quite know how to solve this problem other than to make 2 different OU’s. but if I change:
    $ ldap_dn = “CN = ​​Users, DC = domain, DC = com”;
    to another “CN” than “Users” it comes with errors.
    hope you can help :)

      • Rasmus Lund

        Warning: ldap_search() [function.ldap-search]: Search: No such object in C:\wamp\www\authenticate.php on line 62

        Warning: ldap_get_entries() expects parameter 2 to be resource, boolean given in C:\wamp\www\authenticate.php on line 63

        Warning: array_shift() expects parameter 1 to be array, null given in C:\wamp\www\authenticate.php on line 66

        Warning: Invalid argument supplied for foreach() in C:\wamp\www\authenticate.php on line 72

        If I use “CN=Users” I receive the correct arrays, from the OU=Users in AD.

        • sam

          Your $ldap_dn must be incorrectly using CN instead of OU

          Right click on the object in AD Users & Computers, then go to the Object tab

          If it says ‘Object class: Container’, then use CN=XXX
          If it says ‘Object class: Organizational Unit’, then use OU=XXX

          The Users object is actually a Container and not an OU, this might be the cause of your confusion

  3. David Stetler

    The code worked well, however I did find two issues, 1 I resolved and the other I still have not fully.

    1. The AD setup I was searching through was setup with groups in 1 OU and the users in a separate OU, both at the same level. I added a 2nd $ldap_dn variable with the OU of the Users and then passed that variable into the $results =(ldap_search($ldap,$ldap_dn2, $query);

    2. The other issue I am having is caused by users being setup with the searched group as their primary group. When you set them up this way, AD does not add the user to the member attribute of the group. I am still looking for the best solution to this issue, other than the obvious, just add them all back to the default users as their primary group. Any Ideas?

    Thanks for the great function.

    • sam

      David,

      What you need to do is add primaryGroupID=x to the LDAP query string

      To make it work, you could do this–

      Replace the foreach under ‘Append each group’ with:

      And replace the line under ‘Just looking for membership of one group’ with:

      The code we added will detect if you are using a number instead of a string, and will then check for users that have that number as their primaryGroupID.

      The caveat being you have to use the ID of the group and not the CN, but you can mix it with CN’s since our code checks for numerics:

      513 is the ‘Domain Users’ group

      It’s a bit of a hack, but it should get you in the right direction

      The proper way to do it would probably be to have 2 LDAP queries run: one that looks up the group ID for each/single passed group, and the other would search against both the memberOf with the CN and the primaryGroupID with the ID

    • sam

      No problem, I was in a similar situation and it took a lot of searching and tweaking code to figure it out. Glad I could help someone out there.

  4. I was reading through the posts and thought someone may be able to assist with a small problem we’re having.

    – We built a PHP application that queries and updates Active Directory via Secure LDAP.
    – All the functions work just fine against our test AD Server. For example
    1. Reading a user’s attributes.
    2. Updating user attributes like “employeeID”.
    3. Adding a user to a group.
    4. Removing a user from a group.

    – However, when we run our application in our production environment, all the functions work fine, except for the last one. The application will not remove a user from a group.
    – Below is the info we received from the PHP debug output:

    Warning: ldap_mod_del(): Modify: Server is unwilling to perform in ldap.php on line 40
    Error 53 – Server is unwilling to perform

    Line 40:
    $removed = ldap_mod_del($this->connection, $this->memof, $group_info);

    – This seems like a permissions issue.
    – Any ideas/thoughts on what permissions we need to adjust to allow this delete function to work?

    Thanks!
    Vikas

  5. Huw Davies

    Hi there.

    Thanks for this brilliant function. I have it working great…; appart from a small issue.

    I know it’s a limitation of AD, rather than the script, but I can only pull back 1000 users. Do you know if it returns any kind of pointer, so I could run the script and carry on from 1000 to 1999, and so on…

    • sam

      Possible that it’s not finding the group– maybe due to the DN being incorrect

      Also uncomment the print_r($query) to make sure an actual query is being formulated

      • Jeffrey Meyer

        I did remove the comment tags and the string is correct. Also the DN is all correct and yet the array comes back blank. I get no failed message or error message. This string will not populate though.

        • Jeffrey Meyer

          actually the error i get when i leave all of the print_r’s in is Partial search results returned: Sizelimit exceeded in C:\inetpub\wwwroot\printshop\test.php on line 115

          i only get blank response for the array when i do just this print_r(get_members(“Dept Head”)); and remove all the rest.

          • sam

            Try a:
            print_r($entries);
            under:
            $entries = ldap_get_entries($ldap, $results);

            To see if anything is coming back at all

            The Size limit error you are getting is because one of those queries is returning > 1000 results – which is the LDAP limit- probably the: print_r(get_members()); line

          • Jeffrey Meyer

            i couldn’t reply to what you had said below, there is no option, but…

            I tried adding print_r($entries); with the result being

            [count]=>0

            Also i want to check every folder/ou from AD, but if i try to do

            $ldap_dn = “DC=ad,DC=chsnj,DC=org”;

            I get an error that it cannot connect, i must put an ou to get past that step like this

            $ldap_dn = “OU=Fuld,OU=Locations,DC=ad,DC=chsnj,DC=org”;

          • sam

            That’s because WordPress is keeping the nested threads from getting too deep

            Try adding these two lines above ldap_bind and it should fix your problem:

          • Jeffrey Meyer

            That got rid of the OU problem, but still for some reason i am not getting any results when i tell it was group i want to single out. If i put no group i exceed the limit which tells me it finds the proper results when i do no group but when a group is specified it is failing

          • sam

            Hmmm

            I can’t seem to reproduce the problem

            Where is Dept Head located? Is it nested in other containers? What’s the path to it (remove any info that might be personal)?

          • Jeffrey Meyer

            the group is located in ou=groups, ou=locations, cn=org, cn=chsnj, cn=ad

            but the users are anywhere from cn=org, cn=chsnj, cn=ad down

          • sam

            The function tries to build an exact path to the group

            You need to pass more information in the function call, for example:

            This will be constructed on line 52 as:

          • Jeffrey Meyer

            That did it, now just one last question from me. I now have the array, and i guess i could explode it out, and take what i need out, but is there a simple way to just loop through the content of that array? i tried but it says the array = 0 i wanted to do something like count(thearray) and then do a for loop printing out the ldap information into a dropdown.

          • sam

            Counting the array will give you the number of results found (number of members in the group)

            To loop through the members and output their details, you can use foreach()

            Note that the output is a multidimensional array, so you will need two loops

            Here’s an alternative, referencing individual attributes:

          • Jeffrey Meyer

            Simply amazing man, Thank you for all your help, it is working exactly as i needed it now. Nobody else could help me like that, Thank You!!

  6. Kevin

    Small question but how do I prevent that names with special characters (é, è,…) get into the xml file as: “Véronique” instead of “Véronique”

  7. John

    I am able to return the name and mail attributes, but not others such as department, physicalDeliveryOfficeName, or telephoneNumber. I have tried both Domain User and Domain Admin credentials. Any ideas?

  8. Sam

    Hi Sam,

    I’ve ran into some trouble retrieving thumbnailphoto from the AD.

    How would you do that?

    Kind regards,

    Sam

      • Sam

        That did nothing, if I managed to get the thumbnailPhoto out of the AD it was in some sort of binary code;

        ððÿá PExifII*bh~†(2Ži‡¢XCanonCanon EOS 5D Mark IIIðð2013:05:19 17:35:31š‚à‚è"ˆ'ˆ 0230ð’ ’ ’ (’0’8’ ’ ’@‘’11’’11 0100 ÿÿ¢H¢P¢¤¤¤¤ 2013:04:17 11:44:522013:04:17 11:44:52H¹o@B¬ † è»dnäWµ˜:̦®(¶’ HHÿØÿàJFIFÿÛC $.' ",#(7),01444'9=82<.342ÿÛC 2!!22222222222222222222222222222222222222222222222222ÿÀ–d"ÿÄ ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ %&'()456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓ

        so I added the header to convert the code to the image. But then it would just say that the image contains errors and could not be displayed. As I looked into it, I noticed some HTML was added to that string.

        I was able to display the user’s thumbnailphoto with this

        $userData = $data[$i];

        if(array_key_exists('thumbnailphoto', $userData))
        {
        $fileName = tempnam(sys_get_temp_dir(), 'vex');

        $imageParam = basename($fileName);
        $keyParam = md5($secret . $imageParam);

        $fp = fopen($fileName, "wb") or die("Can't open $fileName for writing");

        fwrite($fp, $userData['thumbnailphoto'][0]);
        fclose($fp);

        echo "";
        } else {
        echo "";
        }

        and


        <?php

        // validate against malicious usage of our script
        $secret = '}84lSB+-cdH{?[';
        $dir = sys_get_temp_dir();

        if(!isset($_GET['image']) || empty($_GET['image']))
        die("Error 1");

        if(!isset($_GET['key']) || empty($_GET['key']))
        die("Error 2");

        if(md5($secret . $_GET['image']) != $_GET['key'])
        die("Error 3");

        if(!file_exists($dir . DIRECTORY_SEPARATOR . $_GET['image']))
        die("Error 4");

        $fp = @fopen($dir . DIRECTORY_SEPARATOR . $_GET['image'], "rb");
        if(!$fp)
        die("Error 5");

        header("Content-type: image/png");
        while(!feof($fp))
        echo fread($fp, 1024);

        fclose($fp);

  9. SuN

    HI

    I have a requirmnet to intigrate my PHP application with MS active directory ..
    also requirment is to only specific group from active directory can log others no access .. can you pls let me know do I need to modify your code ..

    thanks ..SuN

  10. Jeremy Fischer

    Greetings,

    All I’m getting back from this is:

    Array ( )
    Array ( )
    Array ( )
    Array ( )

    for each time it’s called. I can’t tell exactly what’s going on with it. The connect and bind are successful so I can only assume it’s a problem with the ldap_dn?

    I can post my code if it’ll help.

    Thanks!
    J

    • sam

      Try adding these lines above ldap_bind():

      • Jeremy Fischer

        Hiya!

        Thanks for the reply. Actually, I had already added those – the ADS server here won’t do anonymous or non-secure connections for searches/queries.


        // Connect to AD
        //echo "Connecting to LDAP Server...";
        $ldap = ldap_connect($ldap_host,$ldap_port);
        ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);

        //make sure we can go secure
        if (!ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3)) {
        fatal_error("Failed to set LDAP Protocol version to 3, TLS not supported.");
        }

        $ldapbind = ldap_bind($ldap,$adsuser,$adspass);

        I’m certainly open to other ideas. Thanks!

        • sam

          Ok it could be something with the $keep array / foreach

          Not sure if you’ve tried this yet, but do a print_r of $entries under line 63

          Is anything coming back at that point?

  11. Jeremy Fischer

    I did this:


    // Build output array
    foreach($entries as $u) {
    foreach($keep as $x) {
    print_r($entries);
    print_r($keep);
    // Check for attribute
    if(isset($u[$x][0])) $attrval = $u[$x][0]; else $attrval = NULL;

    // Append attribute to output array
    $output[$i][$x] = $attrval;
    print_r($attrval);
    }
    $i++;
    print_r($i);
    }

    echo "Test var_dumps:";
    echo "results output: " . var_dump($results);
    echo "ldap_error output: " . var_dump(ldap_error($ldap));
    echo "ldap_errnr output: " . var_dump(ldap_errno($ldap));

    ldap_close($ldap);
    return $output;

    And this is all that comes back:

    LDAP query test

    LDAP bind successful…
    Test var_dumps:
    resource(5) of type (ldap result)
    results output: string(7) “Success”
    ldap_error output: int(0)
    ldap_errnr output: Array ( )
    LDAP bind successful…
    Test var_dumps:
    resource(7) of type (ldap result)
    results output: string(7) “Success”
    ldap_error output: int(0)
    ldap_errnr output: Array ( )

    I feel like I’m running in circles. :)

    • sam

      Move the print_r($entries) directly under the line that contains:
      $entries = ldap_get_entries($ldap, $results);

      Also make sure PHP error messages are on OR add these lines at the top of your php file to turn them on:

      Just in case an undefined error is being hidden

  12. Jeremy Fischer

    Well, I found a undefined variable with the extra reporting — which was on the TLS setting line, so that’s good. Fixed.

    But still just dumping out this:

    Array ( [count] => 0 )
    Test var_dumps:
    resource(6) of type (ldap result)
    results output: string(7) “Success”
    ldap_error output: int(0)
    ldap_errnr output: Array ( )
    LDAP bind successful…
    Array ( [count] => 0 )
    Test var_dumps:
    resource(8) of type (ldap result)
    results output: string(7) “Success”
    ldap_error output: int(0)
    ldap_errnr output: Array ( )

    • sam

      My only guess is that your query is not matching any results

      Print $query to the page and then try and execute it in AD:
      http://technet.microsoft.com/en-us/library/aa996205(v=exchg.65).aspx#WhereCanWeUseLDAPQueries

      See if you get any results there

      Make sure the elements of your $keep array are valid AD attributes as well

  13. Jeremy Fischer

    So just to give you a followup (and to say thanks for your help!) –

    I’ve got it working with some changes to the group/query scope. I was doing something wrong, still not sure what, but the changes I made let it work.

    I am getting expected results back, though I changed the the $keep array to gather some other attributes.

    I also changed the base DN to the root server container and then did my query based on what I got working in ADUC on my windows virtual box.

    thank you for your patience and help – it got me going the right direction!

    J

  14. hpl76

    Hell’o,

    Can you help me please ? God is busy ^^

    For my SSO issue, I try to display all members of a defined group…in vain ldap_list, search, filter :(

    Should I use memberOf ?

    Regards,

    hpl76

    • sam

      I’m not sure I understand your question. The function queries LDAP using memberOf, given that you provide it a group name as the first argument, get_members(“Test Group”)

    • sam

      Ok it could be an addressing issue, is ‘My own group’ further nested than what you have defined in $ldap_dn?

      You may need to define an explicit path to it like:

  15. hpl76

    What reactivity, awesome ! Thx a lot for replying.

    Your script seems to meet my needs but the result (in my case) is an empty array.

    I got a dynamic group called “specu” in world > dc1 > dc2 > special groups.

    If I ask get_members(“specu”) I got an empty array but there’s a lot of users defined as member. Am I clear ?

    Best regards,

  16. hpl76

    My $ldap_dn is $ldap_dn = “CN=Users,DC=dc1,DC=dc2”; //users society extension.

    get_members(“specu,ou=special groups,dc=dc1,dc=dc2”); returns also an empty array.

    get_members(“cn=specu,ou=special groups,dc=dc1,dc=dc2”); too.

    Any idea ?

    hpl76

    I spent my day on it before to see your function ;)

  17. hpl76

    I have to go. Don’t worry, tomorrow will be an another day ;)

    many many thanks for your help and PHP support !

    Best regards from France.

    hpl76.

    • sam

      Go to the properties of the group and look under Attribute Editor/distinguishedName

      Uncomment line 59 (the print_r($query);)

      Run the script again and see if the print_r value matches the distinguishedName you see in AD

      It could be that your group is not a CN but an OU

  18. hpl76

    Hi Sam !

    The distinguishedName of my dynamic group is :
    CN=Specu,OU=special groups,DC=society,DC=extension

    The result when I print the query is :
    (&(objectClass=user)(objectCategory=person))
    array
    empty

    ?

    I tried this :

    $ldap_dn = CN=Specu,OU=special groups,DC=society,DC=extension
    and
    var_dump(get_members());

    same result but I succeed to display the members of a group when I changed the
    $ldap_dn = CN=Specu,OU=special groups,DC=society,DC=extension
    to
    $ldap_dn = OU=Utilisateurs,OU=Paris,OU=district,DC=society,DC=extension

    var_dump(get_members()); display the members of this group but it’s not the good one grrrr ^^

    What can be the distinction between the 2 entities ? Any idea ?

    BR,

    hpl76.
    ;)

  19. hpl76

    Re re Sam,

    I progressed on this issue and tried this :

    <?php
    $ldap = ldap_connect("IP");
    if ($ldap && $bind = ldap_bind($ldap, "ldapuser@society.extension", "ldappassword")) {
    $query = ldap_search($ldap, "CN=specu,OU=special groups,DC=society,DC=extension", "CN=*");
    $data = ldap_get_entries($ldap, $query);
    for ($i=0; $i

    If I erase CN=specu from the string, all of the CN under the “special groups” appear.

    With CN=specu I got an error ;
    Warning: ldap_search() [function.ldap-search]: Search: No such object in C:\wamp\www\hpl76\ad.php on line 5

    In my AD, I got member objects (in my attribute type) with value beggining by CN=”hpl76&…”

    I don’t understand :(

    hpl76

  20. Peter

    Hi Sam – hopefully you’re still following this article. Your script worked great out of the box. I have a couple of fields that are returning empty, even though I know for certain they are filled in (I can query them in other tools no problem using the same credentials). One field is “employeeID”, and the other is a custom field we’ve added to our AD, called “riboNum”.

    Output looks like this:
    samaccountname=johndoe
    sn=Doe
    givenname=John
    employeeID=empty
    ribonum=empty

    We had an initial challenge to even read those fields in PowerShell, etc, due to permissions and these being considered extended fields. But, we resolved that. This PHP test is using the same credentials, which should hopefully rule out permissions as the problem.

    Any thoughts?

    • sam

      Try a print_r($entries); under the array_shift($entries);

      Can you see any employeeid’s and ribonums dumping to the page at that point?

  21. Peter

    Hmm… no values. Not even seeing the field name anywhere in that print_r content. Is LDAP_GET_ENTRIES able to even see or query extended fields?

    • sam

      LDAP is just not responding to your query with all of the information

      In my test environment I do get employeeid back, I have not tried another custom field

      Try adding these lines above ldap_bind():

  22. Peter

    I figured it out! Your script above queries the Global Catalog (GC) by default (at least it does in my enviro). EmployeeID and custom fields aren’t included in my servers’ GC setup. When I altered your code to do a full LDAP query, the fields were returned properly.

    Things I changed:

    Before:
    $ldap = ldap_connect($ldap_host) or die(“Could not connect to LDAP”);

    After:
    $ldap = ldap_connect(‘ldap://’.$ldap_host) or die(“Could not connect to LDAP”);

    ——————————-
    Independent of this issue, I also found that you can alter:
    $results = ldap_search($ldap,$ldap_dn,$query);

    to this:
    $results = ldap_search($ldap,$ldap_dn,$query,$keep);
    if you want to reduce the number of base items returned in the query to only those that you require. This might let you simplify the $output build routine.

    Thanks for your help, ideas, and the base script.

    • sam

      Peter,

      That’s good to hear. I did not know about the GC stuff. Your use of $keep is more efficient.

      Thanks for the information

  23. varalakshmi

    HI,SAM
    Can please suggest me original attribute names for check message restrictions,group member count,dl name.

    Thanks

      • varalakshmi

        HI SAM,
        THANK YOU for list,but i didnt get the attributes for “check message restrictions”,”group member count”,”dl name” in the list you suggested .Can you Please provide me another list

  24. Diorgenes Mello

    Hi, first, thanks for your script. It help a lot. But I have a situation here. I have two OU for my users OU=Admins,DC=domain,DC=biz and OU=Users,DC=domain,DC=biz, and the groups is on a another location: OU=Groups,DC=domain,DC=biz
    I am not a php programer, but I almost figure out howto fix it, I’ve changed line 52 from:
    $query .= “(memberOf=CN=$group,$ldap_dn)”;
    to:
    $query .= “(memberOf=CN=$group)”;
    and put entire group path on search string:
    print_r(get_members(“Group_ADMINS,OU=Groups,DC=domain,DC=biz”));

    But I still search on one OU only.
    Can you help me?

Leave a Reply

Your email address will not be published. Required fields are marked *