Skip to end of metadata
Go to start of metadata

<ac:macro ac:name="info"><ac:parameter ac:name="title">Zend_Auth_Adapter_Ldap Operator's Guide</ac:parameter><ac:rich-text-body>
<p>For a more practical description of how to use this adapter, please read the <a href="http://framework.zend.com/wiki/display/ZFUSER/Zend_Auth_Adapter_Ldap+Operator%27s+Guide">Zend_Auth_Adapter_Ldap Operator's Guide</a>. This document is a more formal specification.</p></ac:rich-text-body></ac:macro>

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[

Zend Framework: Zend_Auth_Adapter_Ldap Component Proposal

Proposed Component Name Zend_Auth_Adapter_Ldap
Developer Notes http://framework.zend.com/wiki/display/ZFDEV/Zend_Auth_Adapter_Ldap
Proposers Michael B Allen
Darby Felton, Zend liaison
Revision 0.1 - 5 December 2007: Created from content by Michael B Allen.
0.2 - 18 December 2007: Initial pass by Michael B Allen.
0.3 - 8 February 2008: Updated to match latest code by Michael B Allen. (wiki revision: 33)

Table of Contents

1. Overview

Zend_Auth_Adapter_Ldap is proposed as an authentication adapter for Zend_Auth to work with LDAP services.

2. References

3. Component Requirements, Constraints, and Acceptance Criteria

  • This component must conform to the requirements of implementations of the Zend_Auth_Adapter component (i.e. class Zend_Auth_Adapter_Ldap implements Zend_Auth_Adapter_Interface). Any contradiction between this proposal and implementation requirements defined by Zend_Auth_Adapter should be considered an error in this document.
  • This component must use the Zend_Ldap component to perform all LDAP operations.
  • The Zend_Auth_Adapter::authenticate() method must not return Zend_Auth_Result::SUCCESS unless the supplied credentials are positively validated by an LDAP server that is an authority for the account being validated.
  • This component must not throw exceptions for conditions that may occur during normal operation with a properly configured adapter (e.g. authentication failure). All such exceptions will be caught in the adapter's authenticate method and translated into an appropriate Zend_Auth_Result::FAILURE response.
  • This component should throw exceptions for configuration errors, environmental issues and invalid usage (e.g. required options missing, ldap extension unavailable, wrong parameter supplied to method, etc).
  • This component should return Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND if the supplied username is not found in the target LDAP servers or if the username was an empty string (AD may trigger Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID if the supplied username is not found).
  • This component must return Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID if the supplied password was not correct for the supplied username or if the password was an empty string.
  • This component must support optional SSL / TLS encrypted transport. This requirement is satisfied by Zend_Ldap.
  • This component must canonicalize usernames to a form chosen by the operator (e.g. EXAMPLE\username or username@example.com). This requirement is satisfied by Zend_Ldap.

4. Dependencies on Other Framework Components

  • Zend_Auth_Adapter / Zend_Auth_Adapter_Interface - This component is an implementation of the Zend_Auth_Adapter component and, more precisely, its Zend_Auth_Adapter_Interface.
  • Zend_Auth_Adapter_Exception - The Zend_Auth_Adapter_Exception will be used to report configuration errors, environmental issues and invalid usage.
  • Zend_Ldap - The bulk of the work required to authenticate credentials and canonicalize usernames is performed by the Zend_Ldap component.
  • Zend_Ldap_Exception - This component catchs and uses Zend_Ldap_Exception thrown by Zend_Ldap. This is a companion class to Zend_Ldap used to handle unexpected LDAP extension errors and LDAP specific protocol errors (e.g. failed to connect to LDAP server).

5. Theory of Operation

Because Zend_Auth_Adapter_Interface has one method (authenticate()) and because Zend_Ldap handles virtually all of the heavy lifting associated with validating credentials (using the bind() method) and canonicalizing usernames automatically during the bind and with the getCanonicalAccountName() method, the Theory of Operation for this class is simple.

This component iterates over an array of arrays of Zend_Ldap server options and attempts to bind with the supplied credentials. Note that Zend_Ldap::bind() will canonicalize usernames and automatically lookup the account DN if necessary (see the Zend_Ldap proposal for details). If the bind is successful, the canonicalized username is retrieved and Zend_Auth_Result::SUCCESS is returned. If the bind fails, debugging information is collected and the next set of server options is tried. If no server successfully validate the credentials, one of Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND, Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID or Zend_Auth_Result::FAILURE is returned with any error messages from the last iteration of the loop.

With multiple sets of server options, the adapter can authenticate users in multiple domains and provide failover so that if one server is not available, the next one will be queried.

General Zend_Auth_Adapter usage is covered elsewhere but its use with Zend_Auth_Adpater_Ldap is best summarized with the following minimalistic example:

$options = array(
    'server1' => array(
        'host' => 'dc1.w.net',
        'useSsl' => true,
        'accountDomainName' => 'w.net',
        'accountDomainNameShort' = 'W',
        'accountCanonicalForm' => 3,
        'baseDn' => 'CN=Users,DC=w,DC=net',
    ),
    'server2' => array(
        'host' => 's0.foo.net',
        ...

$username = $this->_request->getParam('username');
$password = $this->_request->getParam('password');

$auth = Zend_Auth::getInstance();

require_once 'Zend/Auth/Adapter/Ldap.php';
$adapter = new Zend_Auth_Adapter_Ldap($options, $username, $password);

$result = $auth->authenticate($adapter);

The names of servers (e.g. 'server1' and 'server2' shown above) are largely arbitrary.

The specific options permitted are described in the Zend_Ldap proposal.

The Username and Password Parameters

The second and third constructor parameters are the username and password being authenticated (e.g. the credentials supplied by the user through an HTML login form). These parameters are optional but if they are not specified they must be set using the setUsername() and setPassword() methods. A Zend_Auth_Adapter_Exception will be thrown if the adapter is invoked without a username and password.

The Authenticate Method

The work of a Zend_Auth_Adapter implementation is performed almost entirely in its authenticate() method.

To validate credentials with a specific server the adapter performs an LDAP "bind" operation. Normally a bind would only be performed by LDAP clients that wish to perform directory operations. However, when using LDAP as an authentication service, the act of binding itself is used to validate a username and password. Note that a password must be supplied because some LDAP servers will accept an empty password as an "anonymous bind". As per the Requirements section, this adapter will return Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID if an empty password is supplied.

The Zend_Ldap::bind() method will automatically resolve the account DN and canonicalize usernames if necessary. This leaves little for the authenticate() method to do other than catch expected exceptions (e.g. Zend_Ldap_Exception::LDAP_INVALID_CREDENTIALS, record informative messages and build an appropriate result object. See the Zend_Ldap documentation for details.

The messages returned by the Zend_Auth_Result::getMessages() method on the object returned by the authenticate() method are described as follows:

Messages Array Index String Description
Index 0 An overall message that is suitable for display to users (e.g. "Invalid credentials" or "Password required"). If the authentication was successful, array index 0 will contain an empty string.
Index 1 A detailed error message that is not suitable for display to users but is ideal for informing server opterators as to the cause of the authentication failure. If the authentication was successful, array index 1 will contain an empty string.
Indexes 2 and higher Additional messages collected in chronological order during the authentication process. The final message will contain the string at index 1 if present.

All passwords will be replaced with "*****" in these messages.

6. Milestones / Tasks

  • Milestone 1: [DONE] Create initial prototype.
  • Milestone 2: [DONE] Create documentation necessary to use and test prototype.
  • Milestone 3: Working prototype checked into the incubator.
  • Milestone 4: Create unit tests

7. Class Index

  • Zend_Auth_Adapter_Ldap

8. Use Cases

See examples herein and the Zend_Font - Karol Babioch#5. Theory of Operation section of this proposal and the Operator's Guide.

9. Class Skeletons

]]></ac:plain-text-body></ac:macro>

]]></ac:plain-text-body></ac:macro>

Labels:
None
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Dec 18, 2007

    <p>Developer Notes page is created at <a class="external-link" href="http://framework.zend.com/wiki/display/ZFDEV/Zend_Auth_Adapter_Ldap">http://framework.zend.com/wiki/display/ZFDEV/Zend_Auth_Adapter_Ldap</a>, in case you need it.</p>

    <p>Suggestions: Rename <code>Zend_Auth_Adapter_LdapException</code> to <code>Zend_Auth_Adapter_Ldap_Exception</code>, which extends <code>Zend_Auth_Adapter_Exception</code>, rather than <code>Zend_Exception</code>.</p>

    <blockquote><p>"The names of servers (e.g. 'server1' and 'server2' shown above) are largely arbitrary but must not be an integer."</p></blockquote>

    <p>Why must the keys not be integers?</p>

    <p>The <code>domain_name</code> and <code>domain_name_short</code> option values could have automatic default values derived from the <code>host</code> option value, unless there are good reasons not to do this. For example, where <em><code>ldap.example.com</code></em> is entered as the <code>host</code> option, the automatic derived defaults for <code>domain_name</code> and <code>domain_name_short</code> could be <em><code>example.com</code></em> and <em><code>EXAMPLE</code></em>, respectively. I'm not sure what the best sane default values are, but I think the idea may make sense to consider.</p>

    <p>Will this component not support anonymous binds? Though I think this makes sense, maybe mention why.</p>

    <p>I would prefer that we have unit tests that pass against both Microsoft Active Directory and OpenLDAP. Are there other LDAP service providers against which we should test?</p>

    <p>Protected method names must begin with an underscore (e.g., <code>protected function _getHost()</code>).</p>

    <p>Shouldn't most of the protected methods be public instead (e.g., <code>getHost()</code>)? Which methods will remain protected and why?</p>

    1. Dec 18, 2007

      <blockquote>
      <p>Suggestions: Rename Zend_Auth_Adapter_LdapException to Zend_Auth_Adapter_Ldap_Exception, which extends Zend_Auth_Adapter_Exception, rather than Zend_Exception.</p></blockquote>

      <p>Ok. I didn't do that because I thought it would be less desirable to have an Ldap subdirectory but I'm happy to change that (although ideally I think Zend_Adapter_Ldap_Exception should eventually end up as Zend_Ldap_Exception).</p>

      <blockquote>
      <p> "The names of servers (e.g. 'server1' and 'server2' shown above) are largely arbitrary but must not be an integer."</p>

      <p>Why must the keys not be integers?</p></blockquote>

      <p>Technically, there's no reason. I'm just thinking that all options should be named so that list behavior is reserved. Also, note that most users will probably choose to use Zend_Config which places additional restrictions on key names (e.g. can't have '.' in keys if an INI file is used).</p>

      <blockquote>
      <p>The domain_name and domain_name_short option values could have automatic default values derived from the host option value, unless there are good reasons not to do this. For example, where ldap.example.com is entered as the host option, the automatic derived defaults for domain_name and domain_name_short could be example.com and EXAMPLE, respectively. I'm not sure what the best sane default values are, but I think the idea may make sense to consider.</p></blockquote>

      <p>I'm not sure how I feel about this. If you make them optional, some operators may incorrectly reason that the default values will be correct. They will be correct a high percentage of the time but not always and when they're wrong it could be hard to debug. An operator needs to understand that there are specific names that are correct so requiring these values helps enforce that knowledge. In fact, I think I would prefer to see the names in the config so that it is clear to anyone who looks at it which host is an authority for each domain.</p>

      <blockquote>
      <p>Will this component not support anonymous binds? Though I think this makes sense, maybe mention why.</p></blockquote>

      <p>Actually, I'm pretty sure it does support anonymous binds now. If a probe_password is not supplied I think it will do an anonymous bind. I'll test that and adjust this doc to better describe the conditions necessary for an anonymous bind during the probe operation.</p>

      <blockquote>
      <p>I would prefer that we have unit tests that pass against both Microsoft Active Directory and OpenLDAP. Are there other LDAP service providers against which we should test?</p></blockquote>

      <p>Yes, we need unit tests. I just don't have them. I have tested the latest package against AD and OpenLDAP using the simple Zend_Controller app shipped with the zf-ldap package. I do not have access to any other server types.</p>

      <blockquote>
      <p>Protected method names must begin with an underscore (e.g., protected function _getHost()).</p></blockquote>

      <p>Ok. Will update.</p>

      <blockquote>
      <p>Shouldn't most of the protected methods be public instead (e.g., getHost())? Which methods will remain protected and why?</p></blockquote>

      <p>This is a good question. The rationale is that LDAP implementations vary greatly in implementation and schema so much so that it is impossible to parameterize everyone's needs. Instead, I decided to organize things so that subclasses can override methods if necessary and create an adapter that is more suitable to their specific environment. Users can provide their own name canonicalization, do DNS SRV lookups in getHost, check group membership in bind(), etc. Yes, we could still make those methods public but I can't think of a use case where they would really need them to be public. And we might want to change things as we better understand how the adapter is used so for now I think it's better to only expose the essential parts of the interface. I did the class skelelton quickly but maybe it should be stripped down.</p>

      1. Dec 19, 2007

        <blockquote><p>I'm not sure how I feel about this. If you make them optional, some operators may incorrectly reason that the default values will be correct. They will be correct a high percentage of the time but not always and when they're wrong it could be hard to debug. An operator needs to understand that there are specific names that are correct so requiring these values helps enforce that knowledge. In fact, I think I would prefer to see the names in the config so that it is clear to anyone who looks at it which host is an authority for each domain.</p></blockquote>

        <p>You've convinced me that maybe it's not such a good idea to have such automatic defaults. It would likely cause more problems than it's worth. <ac:emoticon ac:name="smile" /></p>

  2. Dec 25, 2007

    <p>I've nothing to say as regards content, but the param array keys must be camel case (as in other components like Zend_View). So it is domainName and useSsl not domain_name and use_ssl. I do not like that either but there is a <ac:link><ri:page ri:content-title="PHP Coding Standard (draft)" ri:space-key="ZFDEV" /><ac:link-body>coding standard</ac:link-body></ac:link>.</p>

    1. Dec 29, 2007

      <p>I do not see any reference to camelCase array keys in the cited coding standard document. What section and paragraph?</p>

      <p>I do see any reason not to change to camelCase anyway but it would be nice if there were precedence (e.g. other components that use arrays of options w/ camelCase keys)?</p>

      1. Dec 31, 2007

        <p>Yes, there is currently no coding standard that says that array keys must be in camelCase instead of under_score. But, I also agree that camelCase should be our standard, since it is consistent with variable and method naming standards already adopted by ZF coding standards.</p>

        <p>I added a comment to this effect for an associated JIRA issue:
        <a class="external-link" href="http://framework.zend.com/issues/browse/ZF-691">http://framework.zend.com/issues/browse/ZF-691</a></p>

        1. Dec 31, 2007

          <p>Ok. Will do. Note that you might want to propagate that standard back into Zend_Config since that will be the ultimate source of these options keys for most people.</p>

  3. Jan 03, 2008

    <ac:macro ac:name="note"><ac:parameter ac:name="title">Zend Comments</ac:parameter><ac:rich-text-body>
    <p>This proposal is approved for incubator development. During incubation, the LDAP-specific functionality should be migrated to a separate, however lean, Zend_Ldap component, and have Zend_Auth_Adapter_Ldap consume it. The portions of code comprising Zend_Ldap would provide, for the purposes of this proposal, only those functions which Zend_Auth_Adapter_Ldap must utilize, but Zend_Ldap is expected to grow as a component in backward-compatible ways with proposals approved in the future. </p>

    <p>Parts of <code>Zend_Auth_Adapter_Ldap</code> to move to <code>Zend_Ldap</code> (list may be normative, rather than comprehensive):</p>
    <ul>
    <li><code>setServerOptions()</code></li>
    <li><code>getHost()</code></li>
    <li><code>getPort()</code></li>
    <li><code>getProbeUsername()</code></li>
    <li><code>getProbePassword()</code></li>
    <li><code>getBaseDn()</code></li>
    <li><code>getSearchFilterFormat()</code></li>
    <li><code>splitName()</code></li>
    <li><code>getBindUsername()</code></li>
    <li><code>getCanonicalUsername()</code></li>
    <li><code>getSearchFilter()</code></li>
    <li><code>disconnect()</code></li>
    <li><code>connect()</code></li>
    <li><code>bind()</code></li>
    <li><code>getAccount()</code></li>
    <li><code>isMatchingDomain()</code></li>
    </ul>

    <p>The above migrations are not intended to expand the scope of the LDAP authentication adapter, but is instead driven by our desire to 'future-proof' it.</p></ac:rich-text-body></ac:macro>

    1. Jan 06, 2008

      <p>There's a new 0.4.0 package available. I have not yet updated the wiki.</p>

      1. Jan 08, 2008

        <p>Wiki updated w/ Zend_Ldap break-out changes. Also, a newer package is now available. Really need testers now ...</p>

        1. Jan 08, 2008

          <p>Michael,<br />
          I have a lot of LDAP Authentication going that I've written my own adapter for, and will go ahead and employ yours today in an app that I've been assigned. I'll get back with any feedback/problems that I run into. Thank you for the very nice Operator's Guide as well.</p>

          1. Jan 10, 2008

            <p>Great. We could use some feedback. Incedentally the guide is out of date now that Zend_Ldap was broken out.</p>