|  | 
|  | 1 | +# Introduction | 
|  | 2 | + | 
|  | 3 | +zend-ldap lets you perform LDAP operations, including, but not limited to, | 
|  | 4 | +binding, searching and modifying entries in an LDAP directory. | 
|  | 5 | + | 
|  | 6 | +## Theory of operation | 
|  | 7 | + | 
|  | 8 | +This component currently consists of the main `Zend\Ldap\Ldap` class, which | 
|  | 9 | +conceptually represents a binding to a single LDAP server and allows for | 
|  | 10 | +executing operations against a LDAP server such as OpenLDAP or ActiveDirectory | 
|  | 11 | +(AD) servers. The parameters for binding may be provided explicitly or in the | 
|  | 12 | +form of an options array. `Zend\Ldap\Node` provides an object-oriented interface | 
|  | 13 | +for single LDAP nodes and can be used to form a basis for an active-record-like | 
|  | 14 | +interface for a LDAP-based domain model. | 
|  | 15 | + | 
|  | 16 | +The component provides several helper classes to perform operations on LDAP | 
|  | 17 | +entries (`Zend\Ldap\Attribute`) such as setting and retrieving attributes (date | 
|  | 18 | +values, passwords, boolean values, ...), to create and modify LDAP filter | 
|  | 19 | +strings (`Zend\Ldap\Filter`) and to manipulate LDAP distinguished names (DN) | 
|  | 20 | +(`Zend\Ldap\Dn`). | 
|  | 21 | + | 
|  | 22 | +Additionally the component abstracts LDAP schema browsing for OpenLDAP and | 
|  | 23 | +ActiveDirectory servers `Zend\Ldap\Node\Schema` and server information retrieval | 
|  | 24 | +for OpenLDAP-, ActiveDirectory- and Novell eDirectory servers | 
|  | 25 | +(`Zend\Ldap\Node\RootDse`). | 
|  | 26 | + | 
|  | 27 | +Usage of zend-ldap depends on the type of LDAP server, and is best summarized with | 
|  | 28 | +some examples. | 
|  | 29 | + | 
|  | 30 | +If you are using OpenLDAP, consider the following example (note that the | 
|  | 31 | +`bindRequiresDn` option is important if you are **not** using AD): | 
|  | 32 | + | 
|  | 33 | +```php | 
|  | 34 | +use Zend\Ldap\Ldap; | 
|  | 35 | + | 
|  | 36 | +$options = [ | 
|  | 37 | +    'host'              => 's0.foo.net', | 
|  | 38 | +    'username'          => 'CN=user1,DC=foo,DC=net', | 
|  | 39 | +    'password'          => 'pass1', | 
|  | 40 | +    'bindRequiresDn'    => true, | 
|  | 41 | +    'accountDomainName' => 'foo.net', | 
|  | 42 | +    'baseDn'            => 'OU=Sales,DC=foo,DC=net', | 
|  | 43 | +]; | 
|  | 44 | + | 
|  | 45 | +$ldap = new Ldap($options); | 
|  | 46 | +$acctname = $ldap->getCanonicalAccountName('abaker', Ldap::ACCTNAME_FORM_DN); | 
|  | 47 | +echo "$acctname\n"; | 
|  | 48 | +``` | 
|  | 49 | + | 
|  | 50 | +If you are using Microsoft AD: | 
|  | 51 | + | 
|  | 52 | +```php | 
|  | 53 | +use Zend\Ldap\Ldap; | 
|  | 54 | + | 
|  | 55 | +$options = [ | 
|  | 56 | +    'host'                   => 'dc1.w.net', | 
|  | 57 | +    'useStartTls'            => true, | 
|  | 58 | +    'username'               => 'user1@w.net', | 
|  | 59 | +    'password'               => 'pass1', | 
|  | 60 | +    'accountDomainName'      => 'w.net', | 
|  | 61 | +    'accountDomainNameShort' => 'W', | 
|  | 62 | +    'baseDn'                 => 'CN=Users,DC=w,DC=net', | 
|  | 63 | +]; | 
|  | 64 | + | 
|  | 65 | +$ldap = new Ldap($options); | 
|  | 66 | +$acctname = $ldap->getCanonicalAccountName('bcarter', Ldap::ACCTNAME_FORM_DN); | 
|  | 67 | +echo "$acctname\n"; | 
|  | 68 | +``` | 
|  | 69 | + | 
|  | 70 | +Note that we use the `getCanonicalAccountName()` method to retrieve the account | 
|  | 71 | +DN here only because that is what exercises the most of what little code is | 
|  | 72 | +currently present in this class. | 
|  | 73 | + | 
|  | 74 | +### Automatic Username Canonicalization When Binding | 
|  | 75 | + | 
|  | 76 | +If `bind()` is called with a non-DN username but `bindRequiresDN` is `true` | 
|  | 77 | +and no username in DN form was supplied as an option, the bind will fail. | 
|  | 78 | +However, if a username in DN form is supplied in the options array, | 
|  | 79 | +`Zend\Ldap\Ldap` will first bind with that username, retrieve the account DN for | 
|  | 80 | +the username supplied to `bind()` and then re-bind with that DN. | 
|  | 81 | + | 
|  | 82 | +This behavior is critical to [Zend\\Authentication\\Adapter\\Ldap](http://zendframework.github.io/zend-authentication/adapter/ldap/), | 
|  | 83 | +which passes the username supplied by the user directly to `bind()`. | 
|  | 84 | + | 
|  | 85 | +The following example illustrates how the non-DN username 'abaker' can be used | 
|  | 86 | +with `bind()`: | 
|  | 87 | + | 
|  | 88 | +```php | 
|  | 89 | +use Zend\Ldap\Ldap; | 
|  | 90 | + | 
|  | 91 | +$options = [ | 
|  | 92 | +    'host'              => 's0.foo.net', | 
|  | 93 | +    'username'          => 'CN=user1,DC=foo,DC=net', | 
|  | 94 | +    'password'          => 'pass1', | 
|  | 95 | +    'bindRequiresDn'    => true, | 
|  | 96 | +    'accountDomainName' => 'foo.net', | 
|  | 97 | +    'baseDn'            => 'OU=Sales,DC=foo,DC=net', | 
|  | 98 | +]; | 
|  | 99 | + | 
|  | 100 | +$ldap = new Ldap($options); | 
|  | 101 | +$ldap->bind('abaker', 'moonbike55'); | 
|  | 102 | +$acctname = $ldap->getCanonicalAccountName('abaker', Ldap::ACCTNAME_FORM_DN); | 
|  | 103 | +echo "$acctname\n"; | 
|  | 104 | +``` | 
|  | 105 | + | 
|  | 106 | +The `bind()` call in this example sees that the username 'abaker' is not in DN | 
|  | 107 | +form, finds `bindRequiresDn` is `TRUE`, uses `CN=user1,DC=foo,DC=net` and | 
|  | 108 | +`pass1` to bind, retrieves the DN for 'abaker', unbinds and then rebinds with | 
|  | 109 | +the newly discovered `CN=Alice Baker,OU=Sales,DC=foo,DC=net`. | 
|  | 110 | + | 
|  | 111 | +### Account Name Canonicalization | 
|  | 112 | + | 
|  | 113 | +The `accountDomainName` and `accountDomainNameShort` options are used for two | 
|  | 114 | +purposes: (1) they facilitate multi-domain authentication and failover | 
|  | 115 | +capability, and (2) they are also used to canonicalize usernames. Specifically, | 
|  | 116 | +names are canonicalized to the form specified by the `accountCanonicalForm` | 
|  | 117 | +option. This option may one of the following values: | 
|  | 118 | + | 
|  | 119 | +The default canonicalization depends on what account domain name options were | 
|  | 120 | +supplied. If `accountDomainNameShort` was supplied, the default | 
|  | 121 | +`accountCanonicalForm` value is `ACCTNAME_FORM_BACKSLASH`. Otherwise, if | 
|  | 122 | +`accountDomainName` was supplied, the default is `ACCTNAME_FORM_PRINCIPAL`. | 
|  | 123 | + | 
|  | 124 | +Account name canonicalization ensures that the string used to identify an | 
|  | 125 | +account is consistent regardless of what was supplied to `bind()`. For example, | 
|  | 126 | +if the user supplies an account name of `abaker@example.com` or just `abaker` | 
|  | 127 | +and the `accountCanonicalForm` is set to 3, the resulting canonicalized name | 
|  | 128 | +would be `EXAMPLE\\abaker`. | 
|  | 129 | + | 
|  | 130 | +### Multi-domain Authentication and Failover | 
|  | 131 | + | 
|  | 132 | +The `Zend\Ldap\Ldap` component by itself makes no attempt to authenticate with | 
|  | 133 | +multiple servers.  However, `Zend\Ldap\Ldap` is specifically designed to handle | 
|  | 134 | +this scenario gracefully. The required technique is to simply iterate over an | 
|  | 135 | +array of arrays of serve options and attempt to bind with each server. As | 
|  | 136 | +described above `bind()` will automatically canonicalize each name, so it does | 
|  | 137 | +not matter if the user passes `abaker@foo.net` or `Wbcarter` or `cdavis`; the | 
|  | 138 | +`bind()` method will only succeed if the credentials were successfully used in | 
|  | 139 | +the bind. | 
|  | 140 | + | 
|  | 141 | +Consider the following example that illustrates the technique required to | 
|  | 142 | +implement multi-domain authentication and failover: | 
|  | 143 | + | 
|  | 144 | +```php | 
|  | 145 | +use Zend\Ldap\Exception\LdapException; | 
|  | 146 | +use Zend\Ldap\Ldap; | 
|  | 147 | + | 
|  | 148 | +$acctname = 'W\\user2'; | 
|  | 149 | +$password = 'pass2'; | 
|  | 150 | + | 
|  | 151 | +$multiOptions = [ | 
|  | 152 | +    'server1' => [ | 
|  | 153 | +        'host'                   => 's0.foo.net', | 
|  | 154 | +        'username'               => 'CN=user1,DC=foo,DC=net', | 
|  | 155 | +        'password'               => 'pass1', | 
|  | 156 | +        'bindRequiresDn'         => true, | 
|  | 157 | +        'accountDomainName'      => 'foo.net', | 
|  | 158 | +        'accountDomainNameShort' => 'FOO', | 
|  | 159 | +        'accountCanonicalForm'   => 4, // ACCT_FORM_PRINCIPAL | 
|  | 160 | +        'baseDn'                 => 'OU=Sales,DC=foo,DC=net', | 
|  | 161 | +    ], | 
|  | 162 | +    'server2' => [ | 
|  | 163 | +        'host'                   => 'dc1.w.net', | 
|  | 164 | +        'useSsl'                 => true, | 
|  | 165 | +        'username'               => 'user1@w.net', | 
|  | 166 | +        'password'               => 'pass1', | 
|  | 167 | +        'accountDomainName'      => 'w.net', | 
|  | 168 | +        'accountDomainNameShort' => 'W', | 
|  | 169 | +        'accountCanonicalForm'   => 4, // ACCT_FORM_PRINCIPAL | 
|  | 170 | +        'baseDn'                 => 'CN=Users,DC=w,DC=net', | 
|  | 171 | +    ], | 
|  | 172 | +]; | 
|  | 173 | + | 
|  | 174 | +$ldap = new Ldap(); | 
|  | 175 | + | 
|  | 176 | +foreach ($multiOptions as $name => $options) { | 
|  | 177 | +    echo "Trying to bind using server options for '$name'\n"; | 
|  | 178 | + | 
|  | 179 | +    $ldap->setOptions($options); | 
|  | 180 | +    try { | 
|  | 181 | +        $ldap->bind($acctname, $password); | 
|  | 182 | +        $acctname = $ldap->getCanonicalAccountName($acctname); | 
|  | 183 | +        echo "SUCCESS: authenticated $acctname\n"; | 
|  | 184 | +        return; | 
|  | 185 | +    } catch (LdapException $zle) { | 
|  | 186 | +        echo '  ' . $zle->getMessage() . "\n"; | 
|  | 187 | +        if ($zle->getCode() === LdapException::LDAP_X_DOMAIN_MISMATCH) { | 
|  | 188 | +            continue; | 
|  | 189 | +        } | 
|  | 190 | +    } | 
|  | 191 | +} | 
|  | 192 | +``` | 
|  | 193 | + | 
|  | 194 | +If the bind fails for any reason, the next set of server options is tried. | 
|  | 195 | + | 
|  | 196 | +The `getCanonicalAccountName()` call gets the canonical account name that the | 
|  | 197 | +application would presumably use to associate data with such as preferences. The | 
|  | 198 | +`accountCanonicalForm = 4` in all server options ensures that the canonical form | 
|  | 199 | +is consistent regardless of which server was ultimately used. | 
|  | 200 | + | 
|  | 201 | +The special `LDAP_X_DOMAIN_MISMATCH` exception occurs when an account name with | 
|  | 202 | +a domain component was supplied (e.g., `abaker@foo.net` or `FOO\\abaker` and not | 
|  | 203 | +just `abaker`) but the domain component did not match either domain in the | 
|  | 204 | +currently selected server options. This exception indicates that the server is | 
|  | 205 | +not an authority for the account. In this case, the bind will not be performed, | 
|  | 206 | +thereby eliminating unnecessary communication with the server. Note that the | 
|  | 207 | +`continue` instruction has no effect in this example, but in practice for error | 
|  | 208 | +handling and debugging purposes, you will probably want to check for | 
|  | 209 | +`LDAP_X_DOMAIN_MISMATCH` as well as `LDAP_NO_SUCH_OBJECT` and | 
|  | 210 | +`LDAP_INVALID_CREDENTIALS`. | 
|  | 211 | + | 
|  | 212 | +The above code is very similar to code used within | 
|  | 213 | +[Zend\\Authentication\\Adapter\\Ldap](http://zendframework.github.io/zend-authentication/adapter/ldap/). | 
|  | 214 | +In fact,we recommend that you use that authentication adapter for multi-domain + | 
|  | 215 | +failover LDAP based authentication (or copy the code). | 
0 commit comments